diff --git a/.mvn/jvm.config b/.mvn/jvm.config index 32599cefea..e27f6e8f5e 100644 --- a/.mvn/jvm.config +++ b/.mvn/jvm.config @@ -8,3 +8,7 @@ --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/Jenkinsfile b/Jenkinsfile index 0e83b47e2f..4314427b03 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -9,7 +9,7 @@ pipeline { triggers { pollSCM 'H/10 * * * *' - upstream(upstreamProjects: "spring-data-commons/main", threshold: hudson.model.Result.SUCCESS) + upstream(upstreamProjects: "spring-data-commons/4.0.x", threshold: hudson.model.Result.SUCCESS) } options { @@ -20,29 +20,10 @@ pipeline { stages { stage("Docker images") { parallel { - stage('Publish JDK (Java 17) + MongoDB 6.0') { - when { - anyOf { - changeset "ci/openjdk17-mongodb-6.0/**" - changeset "ci/pipeline.properties" - } - } - agent { label 'data' } - options { timeout(time: 30, unit: 'MINUTES') } - - steps { - script { - def image = docker.build("springci/spring-data-with-mongodb-6.0:${p['java.main.tag']}", "--build-arg BASE=${p['docker.java.main.image']} --build-arg MONGODB=${p['docker.mongodb.6.0.version']} ci/openjdk17-mongodb-6.0/") - docker.withRegistry(p['docker.registry'], p['docker.credentials']) { - image.push() - } - } - } - } - stage('Publish JDK (Java 17) + MongoDB 7.0') { + stage('Publish JDK (Java 24) + MongoDB 8.0') { when { anyOf { - changeset "ci/openjdk17-mongodb-7.0/**" + changeset "ci/openjdk24-mongodb-8.0/**" changeset "ci/pipeline.properties" } } @@ -51,7 +32,7 @@ pipeline { steps { script { - def image = docker.build("springci/spring-data-with-mongodb-7.0:${p['java.main.tag']}", "--build-arg BASE=${p['docker.java.main.image']} --build-arg MONGODB=${p['docker.mongodb.7.0.version']} ci/openjdk17-mongodb-7.0/") + def image = docker.build("springci/spring-data-with-mongodb-8.0:${p['java.main.tag']}", "--build-arg BASE=${p['docker.java.main.image']} --build-arg MONGODB=${p['docker.mongodb.7.0.version']} ci/openjdk24-mongodb-8.0/") docker.withRegistry(p['docker.registry'], p['docker.credentials']) { image.push() } @@ -61,7 +42,7 @@ pipeline { stage('Publish JDK (Java.next) + MongoDB 8.0') { when { anyOf { - changeset "ci/openjdk17-mongodb-8.0/**" + changeset "ci/openjdk24-mongodb-8.0/**" changeset "ci/pipeline.properties" } } @@ -70,7 +51,7 @@ pipeline { steps { script { - def image = docker.build("springci/spring-data-with-mongodb-8.0:${p['java.next.tag']}", "--build-arg BASE=${p['docker.java.next.image']} --build-arg MONGODB=${p['docker.mongodb.8.0.version']} ci/openjdk23-mongodb-8.0/") + def image = docker.build("springci/spring-data-with-mongodb-8.0:${p['java.next.tag']}", "--build-arg BASE=${p['docker.java.next.image']} --build-arg MONGODB=${p['docker.mongodb.8.0.version']} ci/openjdk24-mongodb-8.0/") docker.withRegistry(p['docker.registry'], p['docker.credentials']) { image.push() } @@ -99,7 +80,7 @@ pipeline { steps { script { docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) { - docker.image("springci/spring-data-with-mongodb-6.0:${p['java.main.tag']}").inside(p['docker.java.inside.docker']) { + docker.image("springci/spring-data-with-mongodb-8.0:${p['java.main.tag']}").inside(p['docker.java.inside.docker']) { sh 'ci/start-replica.sh' sh 'MAVEN_OPTS="-Duser.name=' + "${p['jenkins.user.name']}" + ' -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-mongodb clean dependency:list test -Dsort -U -B" @@ -118,27 +99,6 @@ pipeline { } } parallel { - stage("test: MongoDB 7.0 (main)") { - 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-mongodb-7.0:${p['java.main.tag']}").inside(p['docker.java.inside.docker']) { - sh 'ci/start-replica.sh' - sh 'MAVEN_OPTS="-Duser.name=' + "${p['jenkins.user.name']}" + ' -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-mongodb clean dependency:list test -Dsort -U -B" - } - } - } - } - } stage("test: MongoDB 8.0") { agent { diff --git a/ci/openjdk17-mongodb-6.0/Dockerfile b/ci/openjdk17-mongodb-6.0/Dockerfile deleted file mode 100644 index fd2580e23a..0000000000 --- a/ci/openjdk17-mongodb-6.0/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -ARG BASE -FROM ${BASE} -# Any ARG statements before FROM are cleared. -ARG MONGODB - -ENV TZ=Etc/UTC -ENV DEBIAN_FRONTEND=noninteractive -ENV MONGO_VERSION=${MONGODB} - -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 apt-transport-https apt-utils gnupg2 wget && \ - # MongoDB 6.0 release signing key - wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | apt-key add - && \ - # Needed when MongoDB creates a 6.0 folder. - echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/6.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-6.0.list && \ - echo ${TZ} > /etc/timezone - -RUN apt-get update && \ - apt-get install -y mongodb-org=${MONGODB} mongodb-org-server=${MONGODB} mongodb-org-shell=${MONGODB} mongodb-org-mongos=${MONGODB} mongodb-org-tools=${MONGODB} && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* diff --git a/ci/openjdk17-mongodb-7.0/Dockerfile b/ci/openjdk17-mongodb-7.0/Dockerfile deleted file mode 100644 index 5701ab9fbc..0000000000 --- a/ci/openjdk17-mongodb-7.0/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -ARG BASE -FROM ${BASE} -# Any ARG statements before FROM are cleared. -ARG MONGODB - -ENV TZ=Etc/UTC -ENV DEBIAN_FRONTEND=noninteractive -ENV MONGO_VERSION=${MONGODB} - -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 apt-transport-https apt-utils gnupg2 wget && \ - # MongoDB 6.0 release signing key - wget -qO - https://www.mongodb.org/static/pgp/server-7.0.asc | apt-key add - && \ - # Needed when MongoDB creates a 7.0 folder. - echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/7.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-7.0.list && \ - echo ${TZ} > /etc/timezone - -RUN apt-get update && \ - apt-get install -y mongodb-org=${MONGODB} mongodb-org-server=${MONGODB} mongodb-org-shell=${MONGODB} mongodb-org-mongos=${MONGODB} mongodb-org-tools=${MONGODB} && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* diff --git a/ci/openjdk23-mongodb-8.0/Dockerfile b/ci/openjdk24-mongodb-8.0/Dockerfile similarity index 100% rename from ci/openjdk23-mongodb-8.0/Dockerfile rename to ci/openjdk24-mongodb-8.0/Dockerfile diff --git a/ci/pipeline.properties b/ci/pipeline.properties index 9eb163fde7..4beebb0dfe 100644 --- a/ci/pipeline.properties +++ b/ci/pipeline.properties @@ -1,19 +1,18 @@ # Java versions -java.main.tag=17.0.13_11-jdk-focal -java.next.tag=23.0.1_11-jdk-noble +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=library/eclipse-temurin:${java.main.tag} docker.java.next.image=library/eclipse-temurin:${java.next.tag} # Supported versions of MongoDB -docker.mongodb.6.0.version=6.0.10 -docker.mongodb.7.0.version=7.0.2 -docker.mongodb.8.0.version=8.0.0 +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 +docker.valkey.8.version=8.1.1 # Docker environment settings docker.java.inside.basic=-v $HOME:/tmp/jenkins-home diff --git a/pom.xml b/pom.xml index 9f4b6bc897..95fc8379d9 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0-SNAPSHOT + 5.0.0-SNAPSHOT pom Spring Data MongoDB @@ -15,7 +15,7 @@ org.springframework.data.build spring-data-parent - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT @@ -26,8 +26,8 @@ multi spring-data-mongodb - 3.5.0-SNAPSHOT - 5.4.0 + 4.0.0-SNAPSHOT + 5.5.0 1.19 diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 58c63dfc97..fc88571622 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0-SNAPSHOT + 5.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 096fd48022..6f34da5660 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0-SNAPSHOT + 5.0.0-SNAPSHOT ../pom.xml @@ -67,12 +67,6 @@ org.springframework spring-core - - - commons-logging - commons-logging - - org.springframework @@ -135,7 +129,7 @@ org.awaitility awaitility - 4.2.2 + ${awaitility} test @@ -146,6 +140,13 @@ true + + net.javacrumbs.json-unit + json-unit-assertj + 4.1.0 + test + + @@ -294,6 +295,12 @@ test + + org.springframework + spring-core-test + test + + org.jetbrains.kotlin @@ -359,8 +366,76 @@ - + + + nullaway + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + com.querydsl + querydsl-apt + ${querydsl} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh} + + + com.google.errorprone + error_prone_core + ${errorprone} + + + com.uber.nullaway + nullaway + ${nullaway} + + + + + + default-compile + none + + + default-testCompile + none + + + java-compile + compile + + compile + + + + -XDcompilePolicy=simple + --should-stop=ifError=FLOW + -Xplugin:ErrorProne -XepDisableAllChecks -Xep:NullAway:ERROR -XepOpt:NullAway:OnlyNullMarked=true -XepOpt:NullAway:TreatGeneratedAsUnannotated=true -XepOpt:NullAway:CustomContractAnnotations=org.springframework.lang.Contract + + + + + java-test-compile + test-compile + + testCompile + + + + + + + + + diff --git a/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/AotRepositoryBenchmark.java b/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/AotRepositoryBenchmark.java new file mode 100644 index 0000000000..ba9da66da4 --- /dev/null +++ b/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/AotRepositoryBenchmark.java @@ -0,0 +1,153 @@ +/* + * 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.mongodb.repository; + +import org.junit.platform.commons.annotation.Testable; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; + +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.microbenchmark.AbstractMicrobenchmark; +import org.springframework.data.mongodb.repository.aot.MongoRepositoryContributor; +import org.springframework.data.mongodb.repository.aot.TestMongoAotRepositoryContext; +import org.springframework.data.mongodb.repository.support.MongoRepositoryFactory; +import org.springframework.data.mongodb.repository.support.QuerydslMongoPredicateExecutor; +import org.springframework.data.mongodb.repository.support.SimpleMongoRepository; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.repository.query.ValueExpressionDelegate; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; + +/** + * Benchmark for AOT repositories. + * + * @author Mark Paluch + */ +@Testable +public class AotRepositoryBenchmark extends AbstractMicrobenchmark { + + @State(Scope.Benchmark) + public static class BenchmarkParameters { + + public static Class aot; + public static TestMongoAotRepositoryContext repositoryContext = new TestMongoAotRepositoryContext( + SmallerPersonRepository.class, + RepositoryComposition.of(RepositoryFragment.structural(SimpleMongoRepository.class), + RepositoryFragment.structural(QuerydslMongoPredicateExecutor.class))); + + MongoClient mongoClient; + MongoTemplate mongoTemplate; + RepositoryComposition.RepositoryFragments fragments; + SmallerPersonRepository repositoryProxy; + + @Setup(Level.Trial) + public void doSetup() { + + mongoClient = MongoClients.create(); + mongoTemplate = new MongoTemplate(mongoClient, "jmh"); + + if (this.aot == null) { + + TestGenerationContext generationContext = new TestGenerationContext(PersonRepository.class); + + new MongoRepositoryContributor(repositoryContext).contribute(generationContext); + + TestCompiler.forSystem().withCompilerOptions("-parameters").with(generationContext).compile(compiled -> { + + try { + this.aot = compiled.getClassLoader().loadClass(SmallerPersonRepository.class.getName() + "Impl__Aot"); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + try { + RepositoryFactoryBeanSupport.FragmentCreationContext creationContext = getCreationContext(repositoryContext); + fragments = RepositoryComposition.RepositoryFragments + .just(aot.getConstructor(MongoOperations.class, RepositoryFactoryBeanSupport.FragmentCreationContext.class) + .newInstance(mongoTemplate, creationContext)); + + this.repositoryProxy = createRepository(fragments); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private RepositoryFactoryBeanSupport.FragmentCreationContext getCreationContext( + TestMongoAotRepositoryContext repositoryContext) { + + RepositoryFactoryBeanSupport.FragmentCreationContext creationContext = new RepositoryFactoryBeanSupport.FragmentCreationContext() { + @Override + public RepositoryMetadata getRepositoryMetadata() { + return repositoryContext.getRepositoryInformation(); + } + + @Override + public ValueExpressionDelegate getValueExpressionDelegate() { + return ValueExpressionDelegate.create(); + } + + @Override + public ProjectionFactory getProjectionFactory() { + return new SpelAwareProxyProjectionFactory(); + } + }; + + return creationContext; + } + + @TearDown(Level.Trial) + public void doTearDown() { + mongoClient.close(); + } + + public SmallerPersonRepository createRepository(RepositoryComposition.RepositoryFragments fragments) { + MongoRepositoryFactory repositoryFactory = new MongoRepositoryFactory(mongoTemplate); + return repositoryFactory.getRepository(SmallerPersonRepository.class, fragments); + } + + } + + @Benchmark + public SmallerPersonRepository repositoryBootstrap(BenchmarkParameters parameters) { + return parameters.createRepository(parameters.fragments); + } + + @Benchmark + public Object findDerived(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findByFirstname("foo"); + } + + @Benchmark + public Object findAnnotated(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findByThePersonsFirstname("foo"); + } + +} diff --git a/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/SmallerPersonRepository.java b/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/SmallerPersonRepository.java new file mode 100644 index 0000000000..bc3868e052 --- /dev/null +++ b/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/SmallerPersonRepository.java @@ -0,0 +1,477 @@ +/* + * Copyright 2010-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.mongodb.repository; + +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + +import org.jspecify.annotations.Nullable; + +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Window; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.core.query.UpdateDefinition; +import org.springframework.data.mongodb.repository.Person.Sex; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.query.Param; + +/** + * Sample repository managing {@link Person} entities. + * + * @author Oliver Gierke + * @author Thomas Darimont + * @author Christoph Strobl + * @author Fırat KÜÇÜK + * @author Mark Paluch + */ +public interface SmallerPersonRepository extends MongoRepository, QuerydslPredicateExecutor { + + /** + * Returns all {@link Person}s with the given lastname. + * + * @param lastname + * @return + */ + List findByLastname(String lastname); + + List findByLastnameStartsWith(String prefix); + + List findByLastnameEndsWith(String postfix); + + /** + * Returns all {@link Person}s with the given lastname ordered by their firstname. + * + * @param lastname + * @return + */ + List findByLastnameOrderByFirstnameAsc(String lastname); + + /** + * Returns the {@link Person}s with the given firstname. Uses {@link Query} annotation to define the query to be + * executed. + * + * @param firstname + * @return + */ + @Query(value = "{ 'lastname' : ?0 }", fields = "{ 'firstname': 1, 'lastname': 1}") + List findByThePersonsLastname(String lastname); + + /** + * Returns the {@link Person}s with the given firstname. Uses {@link Query} annotation to define the query to be + * executed. + * + * @param firstname + * @return + */ + @Query(value = "{ 'firstname' : ?0 }", fields = "{ 'firstname': 1, 'lastname': 1}") + List findByThePersonsFirstname(String firstname); + + // DATAMONGO-871 + @Query(value = "{ 'firstname' : ?0 }") + Person[] findByThePersonsFirstnameAsArray(String firstname); + + /** + * Returns all {@link Person}s with a firstname matching the given one (*-wildcard supported). + * + * @param firstname + * @return + */ + List findByFirstnameLike(@Nullable String firstname); + + List findByFirstnameNotContains(String firstname); + + /** + * Returns all {@link Person}s with a firstname not matching the given one (*-wildcard supported). + * + * @param firstname + * @return + */ + List findByFirstnameNotLike(String firstname); + + List findByFirstnameLikeOrderByLastnameAsc(String firstname, Sort sort); + + List findBySkillsContains(List skills); + + List findBySkillsNotContains(List skills); + + @Query("{'age' : { '$lt' : ?0 } }") + List findByAgeLessThan(int age, Sort sort); + + /** + * Returns a scroll of {@link Person}s with a lastname matching the given one (*-wildcards supported). + * + * @param lastname + * @param scrollPosition + * @return + */ + Window findTop2ByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname, ScrollPosition scrollPosition); + + Window findByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname, ScrollPosition scrollPosition, + Limit limit); + + /** + * Returns a scroll of {@link Person}s applying projections with a lastname matching the given one (*-wildcards + * supported). + * + * @param lastname + * @param pageable + * @return + */ + Window findCursorProjectionByLastnameLike(String lastname, Pageable pageable); + + /** + * Returns a page of {@link Person}s with a lastname matching the given one (*-wildcards supported). + * + * @param lastname + * @param pageable + * @return + */ + Page findByLastnameLike(String lastname, Pageable pageable); + + List findByLastnameLike(String lastname, Sort sort, Limit limit); + + @Query("{ 'lastname' : { '$regex' : '?0', '$options' : 'i'}}") + Page findByLastnameLikeWithPageable(String lastname, Pageable pageable); + + List findByFirstname(String firstname); + + List findByLastnameIgnoreCaseIn(String... lastname); + + /** + * Returns all {@link Person}s with a firstname contained in the given varargs. + * + * @param firstnames + * @return + */ + List findByFirstnameIn(String... firstnames); + + /** + * Returns all {@link Person}s with a firstname not contained in the given collection. + * + * @param firstnames + * @return + */ + List findByFirstnameNotIn(Collection firstnames); + + List findByFirstnameAndLastname(String firstname, String lastname); + + /** + * Returns all {@link Person}s with an age between the two given values. + * + * @param from + * @param to + * @return + */ + List findByAgeBetween(int from, int to); + + /** + * Returns the {@link Person} with the given {@link Address} as shipping address. + * + * @param address + * @return + */ + Person findByShippingAddresses(Address address); + + /** + * Returns all {@link Person}s with the given {@link Address}. + * + * @param address + * @return + */ + List findByAddress(Address address); + + List findByAddressZipCode(String zipCode); + + List findByLastnameLikeAndAgeBetween(String lastname, int from, int to); + + List findByAgeOrLastnameLikeAndFirstnameLike(int age, String lastname, String firstname); + + // TODO: List findByLocationNear(Point point); + + // TODO: List findByLocationWithin(Circle circle); + + // TODO: List findByLocationWithin(Box box); + + // TODO: List findByLocationWithin(Polygon polygon); + + List findBySex(Sex sex); + + List findBySex(Sex sex, Pageable pageable); + + // TODO: List findByNamedQuery(String firstname); + + List findByCreator(User user); + + // DATAMONGO-425 + List findByCreatedAtLessThan(Date date); + + // DATAMONGO-425 + List findByCreatedAtGreaterThan(Date date); + + // DATAMONGO-425 + @Query("{ 'createdAt' : { '$lt' : ?0 }}") + List findByCreatedAtLessThanManually(Date date); + + // DATAMONGO-427 + List findByCreatedAtBefore(Date date); + + // DATAMONGO-427 + List findByCreatedAtAfter(Date date); + + // DATAMONGO-472 + List findByLastnameNot(String lastname); + + // DATAMONGO-600 + List findByCredentials(Credentials credentials); + + // DATAMONGO-636 + long countByLastname(String lastname); + + // DATAMONGO-636 + int countByFirstname(String firstname); + + // DATAMONGO-636 + @Query(value = "{ 'lastname' : ?0 }", count = true) + long someCountQuery(String lastname); + + // DATAMONGO-1454 + boolean existsByFirstname(String firstname); + + // DATAMONGO-1454 + @ExistsQuery(value = "{ 'lastname' : ?0 }") + boolean someExistQuery(String lastname); + + // DATAMONGO-770 + List findByFirstnameIgnoreCase(@Nullable String firstName); + + // DATAMONGO-770 + List findByFirstnameNotIgnoreCase(String firstName); + + // DATAMONGO-770 + List findByFirstnameStartingWithIgnoreCase(String firstName); + + // DATAMONGO-770 + List findByFirstnameEndingWithIgnoreCase(String firstName); + + // DATAMONGO-770 + List findByFirstnameContainingIgnoreCase(String firstName); + + // DATAMONGO-870 + Slice findByAgeGreaterThan(int age, Pageable pageable); + + // DATAMONGO-821 + @Query("{ creator : { $exists : true } }") + Page findByHavingCreator(Pageable page); + + // DATAMONGO-566 + List deleteByLastname(String lastname); + + // DATAMONGO-566 + Long deletePersonByLastname(String lastname); + + // DATAMONGO-1997 + Optional deleteOptionalByLastname(String lastname); + + // DATAMONGO-566 + @Query(value = "{ 'lastname' : ?0 }", delete = true) + List removeByLastnameUsingAnnotatedQuery(String lastname); + + // DATAMONGO-566 + @Query(value = "{ 'lastname' : ?0 }", delete = true) + Long removePersonByLastnameUsingAnnotatedQuery(String lastname); + + // DATAMONGO-893 + Page findByAddressIn(List
address, Pageable page); + + // DATAMONGO-745 + @Query("{firstname:{$in:?0}, lastname:?1}") + Page findByCustomQueryFirstnamesAndLastname(List firstnames, String lastname, Pageable page); + + // DATAMONGO-745 + @Query("{lastname:?0, 'address.street':{$in:?1}}") + Page findByCustomQueryLastnameAndAddressStreetInList(String lastname, List streetNames, + Pageable page); + + // DATAMONGO-950 + List findTop3ByLastnameStartingWith(String lastname); + + // DATAMONGO-950 + Page findTop3ByLastnameStartingWith(String lastname, Pageable pageRequest); + + // DATAMONGO-1865 + Person findFirstBy(); // limits to 1 result if more, just return the first one + + // DATAMONGO-1865 + Person findPersonByLastnameLike(String firstname); // single person, error if more than one + + // DATAMONGO-1865 + Optional findOptionalPersonByLastnameLike(String firstname); // optional still, error when more than one + + // DATAMONGO-1030 + PersonSummaryDto findSummaryByLastname(String lastname); + + PersonSummaryWithOptional findSummaryWithOptionalByLastname(String lastname); + + @Query("{ ?0 : ?1 }") + List findByKeyValue(String key, String value); + + // DATAMONGO-1165 + @Query("{ firstname : { $in : ?0 }}") + Stream findByCustomQueryWithStreamingCursorByFirstnames(List firstnames); + + // DATAMONGO-990 + @Query("{ firstname : ?#{[0]}}") + List findWithSpelByFirstnameForSpELExpressionWithParameterIndexOnly(String firstname); + + // DATAMONGO-990 + @Query("{ firstname : ?#{[0]}, email: ?#{principal.email} }") + List findWithSpelByFirstnameAndCurrentUserWithCustomQuery(String firstname); + + // DATAMONGO-990 + @Query("{ firstname : :#{#firstname}}") + List findWithSpelByFirstnameForSpELExpressionWithParameterVariableOnly(@Param("firstname") String firstname); + + // DATAMONGO-1911 + @Query("{ uniqueId: ?0}") + Person findByUniqueId(UUID uniqueId); + + /** + * Returns the count of {@link Person} with the given firstname. Uses {@link CountQuery} annotation to define the + * query to be executed. + * + * @param firstname + * @return + */ + @CountQuery("{ 'firstname' : ?0 }") // DATAMONGO-1539 + long countByThePersonsFirstname(String firstname); + + /** + * Deletes {@link Person} entities with the given firstname. Uses {@link DeleteQuery} annotation to define the query + * to be executed. + * + * @param firstname + */ + @DeleteQuery("{ 'firstname' : ?0 }") // DATAMONGO-1539 + void deleteByThePersonsFirstname(String firstname); + + // DATAMONGO-1752 + Iterable findOpenProjectionBy(); + + // DATAMONGO-1752 + Iterable findClosedProjectionBy(); + + @Query(sort = "{ age : -1 }") + List findByAgeGreaterThan(int age); + + @Query(sort = "{ age : -1 }") + List findByAgeGreaterThan(int age, Sort sort); + + // TODO: List findByFirstnameRegex(Pattern pattern); + + @Query(value = "{ 'id' : ?0 }", fields = "{ 'fans': { '$slice': [ ?1, ?2 ] } }") + Person findWithSliceInProjection(String id, int skip, int limit); + + @Query(value = "{ 'id' : ?0 }", fields = "{ 'firstname': { '$toUpper': '$firstname' } }") + Person findWithAggregationInProjection(String id); + + @Query(value = "{ 'shippingAddresses' : { '$elemMatch' : { 'city' : { '$eq' : 'lnz' } } } }", + fields = "{ 'shippingAddresses.$': ?0 }") + Person findWithArrayPositionInProjection(int position); + + @Query(value = "{ 'fans' : { '$elemMatch' : { '$ref' : 'user' } } }", fields = "{ 'fans.$': ?0 }") + Person findWithArrayPositionInProjectionWithDbRef(int position); + + @Aggregation("{ '$project': { '_id' : '$lastname' } }") + List findAllLastnames(); + + @Aggregation("{ '$project': { '_id' : '$lastname' } }") + Stream findAllLastnamesAsStream(); + + @Aggregation("{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }") + Stream groupStreamByLastnameAnd(String property); + + @Aggregation("{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }") + List groupByLastnameAnd(String property); + + @Aggregation("{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }") + Slice groupByLastnameAndAsSlice(String property, Pageable pageable); + + @Aggregation("{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }") + List groupByLastnameAnd(String property, Sort sort); + + @Aggregation("{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }") + List groupByLastnameAnd(String property, Pageable page); + + @Aggregation(pipeline = "{ '$group' : { '_id' : null, 'total' : { $sum: '$age' } } }") + int sumAge(); + + @Aggregation(pipeline = "{ '$group' : { '_id' : null, 'total' : { $sum: '$age' } } }") + AggregationResults sumAgeAndReturnAggregationResultWrapper(); + + @Aggregation(pipeline = "{ '$group' : { '_id' : null, 'total' : { $sum: '$age' } } }") + AggregationResults sumAgeAndReturnAggregationResultWrapperWithConcreteType(); + + @Aggregation({ "{ '$match' : { 'lastname' : 'Matthews'} }", + "{ '$project': { _id : 0, firstname : 1, lastname : 1 } }" }) + Iterable findAggregatedClosedInterfaceProjectionBy(); + + @Query(value = "{_id:?0}") + Optional findDocumentById(String id); + + @Query(value = "{ 'firstname' : ?0, 'lastname' : ?1, 'email' : ?2 , 'age' : ?3, 'sex' : ?4, " + + "'createdAt' : ?5, 'skills' : ?6, 'address.street' : ?7, 'address.zipCode' : ?8, " // + + "'address.city' : ?9, 'uniqueId' : ?10, 'credentials.username' : ?11, 'credentials.password' : ?12 }") + Person findPersonByManyArguments(String firstname, String lastname, String email, Integer age, Sex sex, + Date createdAt, List skills, String street, String zipCode, // + String city, UUID uniqueId, String username, String password); + + List findByUnwrappedUserUsername(String username); + + List findByUnwrappedUser(User user); + + int findAndUpdateViaMethodArgAllByLastname(String lastname, UpdateDefinition update); + + @Update("{ '$inc' : { 'visits' : ?1 } }") + int findAndIncrementVisitsByLastname(String lastname, int increment); + + @Query("{ 'lastname' : ?0 }") + @Update("{ '$inc' : { 'visits' : ?1 } }") + int updateAllByLastname(String lastname, int increment); + + @Update(pipeline = { "{ '$set' : { 'visits' : { '$add' : [ '$visits', ?1 ] } } }" }) + void findAndIncrementVisitsViaPipelineByLastname(String lastname, int increment); + + @Update("{ '$inc' : { 'visits' : ?#{[1]} } }") + int findAndIncrementVisitsUsingSpELByLastname(String lastname, int increment); + + @Update("{ '$push' : { 'shippingAddresses' : ?1 } }") + int findAndPushShippingAddressByEmail(String email, Address address); + + @Query("{ 'age' : null }") + Person findByQueryWithNullEqualityCheck(); + + List findBySpiritAnimal(User user); + +} diff --git a/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/SmallerRepositoryBenchmark.java b/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/SmallerRepositoryBenchmark.java new file mode 100644 index 0000000000..f461a22d31 --- /dev/null +++ b/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/SmallerRepositoryBenchmark.java @@ -0,0 +1,83 @@ +/* + * 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.mongodb.repository; + +import org.junit.platform.commons.annotation.Testable; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; + +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.microbenchmark.AbstractMicrobenchmark; +import org.springframework.data.mongodb.repository.support.MongoRepositoryFactory; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; + +/** + * Benchmark for AOT repositories. + * + * @author Mark Paluch + */ +@Testable +public class SmallerRepositoryBenchmark extends AbstractMicrobenchmark { + + @State(Scope.Benchmark) + public static class BenchmarkParameters { + + MongoClient mongoClient; + MongoTemplate mongoTemplate; + SmallerPersonRepository repositoryProxy; + + @Setup(Level.Trial) + public void doSetup() { + + mongoClient = MongoClients.create(); + mongoTemplate = new MongoTemplate(mongoClient, "jmh"); + repositoryProxy = createRepository(); + } + + @TearDown(Level.Trial) + public void doTearDown() { + mongoClient.close(); + } + + public SmallerPersonRepository createRepository() { + MongoRepositoryFactory repositoryFactory = new MongoRepositoryFactory(mongoTemplate); + return repositoryFactory.getRepository(SmallerPersonRepository.class); + } + + } + + @Benchmark + public SmallerPersonRepository repositoryBootstrap(BenchmarkParameters parameters) { + return parameters.createRepository(); + } + + @Benchmark + public Object findDerived(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findByFirstname("foo"); + } + + @Benchmark + public Object findAnnotated(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findByThePersonsFirstname("foo"); + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BindableMongoExpression.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BindableMongoExpression.java index 1f6875c080..3ae41aad35 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BindableMongoExpression.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BindableMongoExpression.java @@ -20,9 +20,10 @@ import org.bson.Document; import org.bson.codecs.DocumentCodec; import org.bson.codecs.configuration.CodecRegistry; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -31,8 +32,7 @@ * A {@link MongoExpression} using the {@link ParameterBindingDocumentCodec} for parsing a raw ({@literal json}) * expression. The expression will be wrapped within { ... } if necessary. The actual parsing and parameter * binding of placeholders like {@code ?0} is delayed upon first call on the target {@link Document} via - * {@link #toDocument()}. - *
+ * {@link #toDocument()}.
* *
  * $toUpper : $name                -> { '$toUpper' : '$name' }
@@ -55,7 +55,7 @@ public class BindableMongoExpression implements MongoExpression {
 
 	private final @Nullable CodecRegistryProvider codecRegistryProvider;
 
-	private final @Nullable Object[] args;
+	private final Object @Nullable [] args;
 
 	private final Lazy target;
 
@@ -63,9 +63,9 @@ public class BindableMongoExpression implements MongoExpression {
 	 * Create a new instance of {@link BindableMongoExpression}.
 	 *
 	 * @param expression must not be {@literal null}.
-	 * @param args can be {@literal null}.
+	 * @param args must not be {@literal null} but may contain {@literal null} elements.
 	 */
-	public BindableMongoExpression(String expression, @Nullable Object[] args) {
+	public BindableMongoExpression(String expression, Object @Nullable [] args) {
 		this(expression, null, args);
 	}
 
@@ -74,10 +74,10 @@ public BindableMongoExpression(String expression, @Nullable Object[] args) {
 	 *
 	 * @param expression must not be {@literal null}.
 	 * @param codecRegistryProvider can be {@literal null}.
-	 * @param args can be {@literal null}.
+	 * @param args must not be {@literal null} but may contain {@literal null} elements.
 	 */
 	public BindableMongoExpression(String expression, @Nullable CodecRegistryProvider codecRegistryProvider,
-			@Nullable Object[] args) {
+			Object @Nullable [] args) {
 
 		Assert.notNull(expression, "Expression must not be null");
 
@@ -93,6 +93,7 @@ public BindableMongoExpression(String expression, @Nullable CodecRegistryProvide
 	 * @param codecRegistry must not be {@literal null}.
 	 * @return new instance of {@link BindableMongoExpression}.
 	 */
+	@Contract("_ -> new")
 	public BindableMongoExpression withCodecRegistry(CodecRegistry codecRegistry) {
 		return new BindableMongoExpression(expressionString, () -> codecRegistry, args);
 	}
@@ -103,6 +104,7 @@ public BindableMongoExpression withCodecRegistry(CodecRegistry codecRegistry) {
 	 * @param args must not be {@literal null}.
 	 * @return new instance of {@link BindableMongoExpression}.
 	 */
+	@Contract("_ -> new")
 	public BindableMongoExpression bind(Object... args) {
 		return new BindableMongoExpression(expressionString, codecRegistryProvider, args);
 	}
@@ -139,7 +141,7 @@ private Document parse() {
 
 	private static String wrapJsonIfNecessary(String json) {
 
-		if(!StringUtils.hasText(json)) {
+		if (!StringUtils.hasText(json)) {
 			return json;
 		}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BulkOperationException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BulkOperationException.java
index b36382a58e..12d8c966af 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BulkOperationException.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BulkOperationException.java
@@ -17,6 +17,7 @@
 
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.DataAccessException;
 
 import com.mongodb.MongoBulkWriteException;
@@ -40,10 +41,10 @@ public class BulkOperationException extends DataAccessException {
 	/**
 	 * Creates a new {@link BulkOperationException} with the given message and source {@link MongoBulkWriteException}.
 	 *
-	 * @param message must not be {@literal null}.
+	 * @param message can be {@literal null}.
 	 * @param source must not be {@literal null}.
 	 */
-	public BulkOperationException(String message, MongoBulkWriteException source) {
+	public BulkOperationException(@Nullable String message, MongoBulkWriteException source) {
 
 		super(message, source);
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ClientSessionException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ClientSessionException.java
index 53acf65470..c59eecb43a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ClientSessionException.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ClientSessionException.java
@@ -15,8 +15,8 @@
  */
 package org.springframework.data.mongodb;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.NonTransientDataAccessException;
-import org.springframework.lang.Nullable;
 
 /**
  * {@link NonTransientDataAccessException} specific to MongoDB {@link com.mongodb.session.ClientSession} related data
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolver.java
index c07e2dbe4a..87201ef9ee 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolver.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolver.java
@@ -18,7 +18,7 @@
 import java.util.Map;
 import java.util.Set;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * Default implementation of {@link MongoTransactionOptions} using {@literal mongo:} as {@link #getLabelPrefix() label
@@ -42,9 +42,8 @@ public MongoTransactionOptions convert(Map options) {
 		return SimpleMongoTransactionOptions.of(options);
 	}
 
-	@Nullable
 	@Override
-	public String getLabelPrefix() {
+	public @Nullable String getLabelPrefix() {
 		return PREFIX;
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDatabaseUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDatabaseUtils.java
index f73f9fb7ed..042a5ba1d3 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDatabaseUtils.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDatabaseUtils.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.transaction.support.ResourceHolderSynchronization;
 import org.springframework.transaction.support.TransactionSynchronization;
 import org.springframework.transaction.support.TransactionSynchronizationManager;
@@ -29,8 +29,7 @@
 /**
  * Helper class for managing a {@link MongoDatabase} instances via {@link MongoDatabaseFactory}. Used for obtaining
  * {@link ClientSession session bound} resources, such as {@link MongoDatabase} and
- * {@link com.mongodb.client.MongoCollection} suitable for transactional usage.
- * 
+ * {@link com.mongodb.client.MongoCollection} suitable for transactional usage.
* Note: Intended for internal usage only. * * @author Christoph Strobl @@ -42,8 +41,7 @@ public class MongoDatabaseUtils { /** * Obtain the default {@link MongoDatabase database} form the given {@link MongoDatabaseFactory factory} using - * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}. - *
+ * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current * {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}. * @@ -55,8 +53,7 @@ public static MongoDatabase getDatabase(MongoDatabaseFactory factory) { } /** - * Obtain the default {@link MongoDatabase database} form the given {@link MongoDatabaseFactory factory}. - *
+ * Obtain the default {@link MongoDatabase database} form the given {@link MongoDatabaseFactory factory}.
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current * {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}. * @@ -70,8 +67,7 @@ public static MongoDatabase getDatabase(MongoDatabaseFactory factory, SessionSyn /** * Obtain the {@link MongoDatabase database} with given name form the given {@link MongoDatabaseFactory factory} using - * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}. - *
+ * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current * {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}. * @@ -139,8 +135,7 @@ public static boolean isTransactionActive(MongoDatabaseFactory dbFactory) { return resourceHolder != null && resourceHolder.hasActiveTransaction(); } - @Nullable - private static ClientSession doGetSession(MongoDatabaseFactory dbFactory, + private static @Nullable ClientSession doGetSession(MongoDatabaseFactory dbFactory, SessionSynchronization sessionSynchronization) { MongoResourceHolder resourceHolder = (MongoResourceHolder) TransactionSynchronizationManager.getResource(dbFactory); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoResourceHolder.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoResourceHolder.java index a1e8344a9f..81c25d0998 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoResourceHolder.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoResourceHolder.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.support.ResourceHolderSupport; @@ -23,8 +23,7 @@ /** * MongoDB specific {@link ResourceHolderSupport resource holder}, wrapping a {@link ClientSession}. - * {@link MongoTransactionManager} binds instances of this class to the thread. - *
+ * {@link MongoTransactionManager} binds instances of this class to the thread.
* Note: Intended for internal usage only. * * @author Christoph Strobl diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionException.java index 4215479f62..3d7bec6780 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionException.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionException.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A specific {@link ClientSessionException} related to issues with a transaction such as aborted or non existing diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java index eda657f5f1..1f97bb69e9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java @@ -15,8 +15,8 @@ */ package org.springframework.data.mongodb; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; import org.springframework.transaction.TransactionSystemException; @@ -36,19 +36,15 @@ /** * A {@link org.springframework.transaction.PlatformTransactionManager} implementation that manages - * {@link ClientSession} based transactions for a single {@link MongoDatabaseFactory}. - *
- * Binds a {@link ClientSession} from the specified {@link MongoDatabaseFactory} to the thread. - *
+ * {@link ClientSession} based transactions for a single {@link MongoDatabaseFactory}.
+ * Binds a {@link ClientSession} from the specified {@link MongoDatabaseFactory} to the thread.
* {@link TransactionDefinition#isReadOnly() Readonly} transactions operate on a {@link ClientSession} and enable causal * consistency, and also {@link ClientSession#startTransaction() start}, {@link ClientSession#commitTransaction() - * commit} or {@link ClientSession#abortTransaction() abort} a transaction. - *
+ * commit} or {@link ClientSession#abortTransaction() abort} a transaction.
* Application code is required to retrieve the {@link com.mongodb.client.MongoDatabase} via * {@link MongoDatabaseUtils#getDatabase(MongoDatabaseFactory)} instead of a standard * {@link MongoDatabaseFactory#getMongoDatabase()} call. Spring classes such as - * {@link org.springframework.data.mongodb.core.MongoTemplate} use this strategy implicitly. - *
+ * {@link org.springframework.data.mongodb.core.MongoTemplate} use this strategy implicitly.
* By default failure of a {@literal commit} operation raises a {@link TransactionSystemException}. One may override * {@link #doCommit(MongoTransactionObject)} to implement the * Retry Commit Operation @@ -80,7 +76,9 @@ public class MongoTransactionManager extends AbstractPlatformTransactionManager * @see #setTransactionSynchronization(int) */ public MongoTransactionManager() { + this.transactionOptionsResolver = MongoTransactionOptionsResolver.defaultResolver(); + this.options = MongoTransactionOptions.NONE; } /** @@ -151,7 +149,8 @@ protected void doBegin(Object transaction, TransactionDefinition definition) thr } try { - MongoTransactionOptions mongoTransactionOptions = transactionOptionsResolver.resolve(definition).mergeWith(options); + MongoTransactionOptions mongoTransactionOptions = transactionOptionsResolver.resolve(definition) + .mergeWith(options); mongoTransactionObject.startTransaction(mongoTransactionOptions.toDriverOptions()); } catch (MongoException ex) { throw new TransactionSystemException(String.format("Could not start Mongo transaction for session %s.", @@ -206,6 +205,7 @@ protected final void doCommit(DefaultTransactionStatus status) throws Transactio * By default those labels are ignored, nevertheless one might check for * {@link MongoException#UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL transient commit errors labels} and retry the the * commit.
+ * *
 	 * 
 	 * int retries = 3;
@@ -302,8 +302,7 @@ public void setOptions(@Nullable TransactionOptions options) {
 	 *
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	public MongoDatabaseFactory getDatabaseFactory() {
+	public @Nullable MongoDatabaseFactory getDatabaseFactory() {
 		return databaseFactory;
 	}
 
@@ -461,8 +460,7 @@ void closeSession() {
 			}
 		}
 
-		@Nullable
-		public ClientSession getSession() {
+		public @Nullable ClientSession getSession() {
 			return resourceHolder != null ? resourceHolder.getSession() : null;
 		}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptions.java
index e411bd5d2d..04bcd36e35 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptions.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptions.java
@@ -19,15 +19,16 @@
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.ReadConcernAware;
 import org.springframework.data.mongodb.core.ReadPreferenceAware;
 import org.springframework.data.mongodb.core.WriteConcernAware;
-import org.springframework.lang.Nullable;
 
 import com.mongodb.ReadConcern;
 import com.mongodb.ReadPreference;
 import com.mongodb.TransactionOptions;
 import com.mongodb.WriteConcern;
+import org.springframework.lang.Contract;
 
 /**
  * Options to be applied within a specific transaction scope.
@@ -43,27 +44,23 @@ public interface MongoTransactionOptions
 	 */
 	MongoTransactionOptions NONE = new MongoTransactionOptions() {
 
-		@Nullable
 		@Override
-		public Duration getMaxCommitTime() {
+		public @Nullable Duration getMaxCommitTime() {
 			return null;
 		}
 
-		@Nullable
 		@Override
-		public ReadConcern getReadConcern() {
+		public @Nullable ReadConcern getReadConcern() {
 			return null;
 		}
 
-		@Nullable
 		@Override
-		public ReadPreference getReadPreference() {
+		public @Nullable ReadPreference getReadPreference() {
 			return null;
 		}
 
-		@Nullable
 		@Override
-		public WriteConcern getWriteConcern() {
+		public @Nullable WriteConcern getWriteConcern() {
 			return null;
 		}
 	};
@@ -76,6 +73,7 @@ public WriteConcern getWriteConcern() {
 	 * @return new instance of {@link MongoTransactionOptions} or this if {@literal fallbackOptions} is {@literal null} or
 	 *         {@link #NONE}.
 	 */
+	@Contract("null -> this")
 	default MongoTransactionOptions mergeWith(@Nullable MongoTransactionOptions fallbackOptions) {
 
 		if (fallbackOptions == null || MongoTransactionOptions.NONE.equals(fallbackOptions)) {
@@ -84,30 +82,26 @@ default MongoTransactionOptions mergeWith(@Nullable MongoTransactionOptions fall
 
 		return new MongoTransactionOptions() {
 
-			@Nullable
 			@Override
-			public Duration getMaxCommitTime() {
+			public @Nullable Duration getMaxCommitTime() {
 				return MongoTransactionOptions.this.hasMaxCommitTime() ? MongoTransactionOptions.this.getMaxCommitTime()
 						: fallbackOptions.getMaxCommitTime();
 			}
 
-			@Nullable
 			@Override
-			public ReadConcern getReadConcern() {
+			public @Nullable ReadConcern getReadConcern() {
 				return MongoTransactionOptions.this.hasReadConcern() ? MongoTransactionOptions.this.getReadConcern()
 						: fallbackOptions.getReadConcern();
 			}
 
-			@Nullable
 			@Override
-			public ReadPreference getReadPreference() {
+			public @Nullable ReadPreference getReadPreference() {
 				return MongoTransactionOptions.this.hasReadPreference() ? MongoTransactionOptions.this.getReadPreference()
 						: fallbackOptions.getReadPreference();
 			}
 
-			@Nullable
 			@Override
-			public WriteConcern getWriteConcern() {
+			public @Nullable WriteConcern getWriteConcern() {
 				return MongoTransactionOptions.this.hasWriteConcern() ? MongoTransactionOptions.this.getWriteConcern()
 						: fallbackOptions.getWriteConcern();
 			}
@@ -128,8 +122,8 @@ default  T map(Function mappingFunction) {
 	 * @return MongoDB driver native {@link TransactionOptions}.
 	 * @see MongoTransactionOptions#map(Function)
 	 */
-	@Nullable
-	default TransactionOptions toDriverOptions() {
+	@SuppressWarnings("NullAway")
+	default @Nullable TransactionOptions toDriverOptions() {
 
 		return map(it -> {
 
@@ -157,7 +151,7 @@ default TransactionOptions toDriverOptions() {
 	/**
 	 * Factory method to wrap given MongoDB driver native {@link TransactionOptions} into {@link MongoTransactionOptions}.
 	 *
-	 * @param options
+	 * @param options can be {@literal null}.
 	 * @return {@link MongoTransactionOptions#NONE} if given object is {@literal null}.
 	 */
 	static MongoTransactionOptions of(@Nullable TransactionOptions options) {
@@ -168,35 +162,30 @@ static MongoTransactionOptions of(@Nullable TransactionOptions options) {
 
 		return new MongoTransactionOptions() {
 
-			@Nullable
 			@Override
-			public Duration getMaxCommitTime() {
+			public @Nullable Duration getMaxCommitTime() {
 
 				Long millis = options.getMaxCommitTime(TimeUnit.MILLISECONDS);
 				return millis != null ? Duration.ofMillis(millis) : null;
 			}
 
-			@Nullable
 			@Override
-			public ReadConcern getReadConcern() {
+			public @Nullable ReadConcern getReadConcern() {
 				return options.getReadConcern();
 			}
 
-			@Nullable
 			@Override
-			public ReadPreference getReadPreference() {
+			public @Nullable ReadPreference getReadPreference() {
 				return options.getReadPreference();
 			}
 
-			@Nullable
 			@Override
-			public WriteConcern getWriteConcern() {
+			public @Nullable WriteConcern getWriteConcern() {
 				return options.getWriteConcern();
 			}
 
-			@Nullable
 			@Override
-			public TransactionOptions toDriverOptions() {
+			public @Nullable TransactionOptions toDriverOptions() {
 				return options;
 			}
 		};
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java
index b73b079a99..c4bdbcca53 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java
@@ -18,7 +18,7 @@
 import java.util.Map;
 import java.util.stream.Collectors;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.transaction.TransactionDefinition;
 import org.springframework.transaction.interceptor.TransactionAttribute;
 import org.springframework.util.Assert;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseUtils.java
index f397818a4c..3d1c2ee89c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseUtils.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseUtils.java
@@ -18,7 +18,7 @@
 import reactor.core.publisher.Mono;
 import reactor.util.context.Context;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.transaction.NoTransactionException;
 import org.springframework.transaction.reactive.ReactiveResourceSynchronization;
 import org.springframework.transaction.reactive.TransactionSynchronization;
@@ -35,8 +35,7 @@
 /**
  * Helper class for managing reactive {@link MongoDatabase} instances via {@link ReactiveMongoDatabaseFactory}. Used for
  * obtaining {@link ClientSession session bound} resources, such as {@link MongoDatabase} and {@link MongoCollection}
- * suitable for transactional usage.
- * 
+ * suitable for transactional usage.
* Note: Intended for internal usage only. * * @author Mark Paluch @@ -74,8 +73,7 @@ public static Mono isTransactionActive(ReactiveMongoDatabaseFactory dat /** * Obtain the default {@link MongoDatabase database} form the given {@link ReactiveMongoDatabaseFactory factory} using - * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}. - *
+ * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}. * @@ -103,32 +101,32 @@ public static Mono getDatabase(ReactiveMongoDatabaseFactory facto /** * Obtain the {@link MongoDatabase database} with given name form the given {@link ReactiveMongoDatabaseFactory - * factory} using {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}. - *
+ * factory} using {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}. * - * @param dbName the name of the {@link MongoDatabase} to get. + * @param dbName the name of the {@link MongoDatabase} to get. If {@literal null} the default database of the + * {@link ReactiveMongoDatabaseFactory}. * @param factory the {@link ReactiveMongoDatabaseFactory} to get the {@link MongoDatabase} from. * @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}. */ - public static Mono getDatabase(String dbName, ReactiveMongoDatabaseFactory factory) { + public static Mono getDatabase(@Nullable String dbName, ReactiveMongoDatabaseFactory factory) { return doGetMongoDatabase(dbName, factory, SessionSynchronization.ON_ACTUAL_TRANSACTION); } /** * Obtain the {@link MongoDatabase database} with given name form the given {@link ReactiveMongoDatabaseFactory - * factory}. - *
+ * factory}.
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}. * - * @param dbName the name of the {@link MongoDatabase} to get. + * @param dbName the name of the {@link MongoDatabase} to get. If {@literal null} the default database of the * + * {@link ReactiveMongoDatabaseFactory}. * @param factory the {@link ReactiveMongoDatabaseFactory} to get the {@link MongoDatabase} from. * @param sessionSynchronization the synchronization to use. Must not be {@literal null}. * @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}. */ - public static Mono getDatabase(String dbName, ReactiveMongoDatabaseFactory factory, + public static Mono getDatabase(@Nullable String dbName, ReactiveMongoDatabaseFactory factory, SessionSynchronization sessionSynchronization) { return doGetMongoDatabase(dbName, factory, sessionSynchronization); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoResourceHolder.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoResourceHolder.java index 33caa5e7fe..d01364b202 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoResourceHolder.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoResourceHolder.java @@ -15,16 +15,15 @@ */ package org.springframework.data.mongodb; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.ResourceHolderSupport; import com.mongodb.reactivestreams.client.ClientSession; /** * MongoDB specific resource holder, wrapping a {@link ClientSession}. {@link ReactiveMongoTransactionManager} binds - * instances of this class to the subscriber context. - *
+ * instances of this class to the subscriber context.
* Note: Intended for internal usage only. * * @author Mark Paluch @@ -103,8 +102,7 @@ boolean hasSession() { * @param session * @return */ - @Nullable - public ClientSession setSessionIfAbsent(@Nullable ClientSession session) { + public @Nullable ClientSession setSessionIfAbsent(@Nullable ClientSession session) { if (!hasSession()) { setSession(session); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java index 2c65c26b79..4f293c8ed6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java @@ -17,8 +17,8 @@ import reactor.core.publisher.Mono; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; import org.springframework.transaction.TransactionSystemException; @@ -64,7 +64,7 @@ public class ReactiveMongoTransactionManager extends AbstractReactiveTransactionManager implements InitializingBean { private @Nullable ReactiveMongoDatabaseFactory databaseFactory; - private @Nullable MongoTransactionOptions options; + private MongoTransactionOptions options; private final MongoTransactionOptionsResolver transactionOptionsResolver; /** @@ -79,7 +79,9 @@ public class ReactiveMongoTransactionManager extends AbstractReactiveTransaction * @see #setDatabaseFactory(ReactiveMongoDatabaseFactory) */ public ReactiveMongoTransactionManager() { + this.transactionOptionsResolver = MongoTransactionOptionsResolver.defaultResolver(); + this.options = MongoTransactionOptions.NONE; } /** @@ -98,7 +100,7 @@ public ReactiveMongoTransactionManager(ReactiveMongoDatabaseFactory databaseFact * starting a new transaction. * * @param databaseFactory must not be {@literal null}. - * @param options can be {@literal null}. + * @param options can be {@literal null}. Will default {@link MongoTransactionOptions#NONE} if {@literal null}. */ public ReactiveMongoTransactionManager(ReactiveMongoDatabaseFactory databaseFactory, @Nullable TransactionOptions options) { @@ -112,7 +114,8 @@ public ReactiveMongoTransactionManager(ReactiveMongoDatabaseFactory databaseFact * * @param databaseFactory must not be {@literal null}. * @param transactionOptionsResolver must not be {@literal null}. - * @param defaultTransactionOptions can be {@literal null}. + * @param defaultTransactionOptions can be {@literal null}. Will default {@link MongoTransactionOptions#NONE} if + * {@literal null}. * @since 4.3 */ public ReactiveMongoTransactionManager(ReactiveMongoDatabaseFactory databaseFactory, @@ -124,7 +127,7 @@ public ReactiveMongoTransactionManager(ReactiveMongoDatabaseFactory databaseFact this.databaseFactory = databaseFactory; this.transactionOptionsResolver = transactionOptionsResolver; - this.options = defaultTransactionOptions; + this.options = defaultTransactionOptions != null ? defaultTransactionOptions : MongoTransactionOptions.NONE; } @Override @@ -318,8 +321,7 @@ public void setOptions(@Nullable TransactionOptions options) { * * @return can be {@literal null}. */ - @Nullable - public ReactiveMongoDatabaseFactory getDatabaseFactory() { + public @Nullable ReactiveMongoDatabaseFactory getDatabaseFactory() { return databaseFactory; } @@ -470,8 +472,7 @@ void closeSession() { } } - @Nullable - public ClientSession getSession() { + public @Nullable ClientSession getSession() { return resourceHolder != null ? resourceHolder.getSession() : null; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionAwareMethodInterceptor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionAwareMethodInterceptor.java index 93dbf5db69..ec30478a54 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionAwareMethodInterceptor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionAwareMethodInterceptor.java @@ -22,8 +22,8 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodClassKey; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; @@ -34,8 +34,7 @@ /** * {@link MethodInterceptor} implementation looking up and invoking an alternative target method having - * {@link ClientSession} as its first argument. This allows seamless integration with the existing code base. - *
+ * {@link ClientSession} as its first argument. This allows seamless integration with the existing code base.
* The {@link MethodInterceptor} is aware of methods on {@code MongoCollection} that my return new instances of itself * like (eg. {@link com.mongodb.reactivestreams.client.MongoCollection#withWriteConcern(WriteConcern)} and decorate them * if not already proxied. @@ -95,13 +94,13 @@ public SessionAwareMethodInterceptor(ClientSession session, T target, Class< this.sessionType = sessionType; } - @Nullable @Override - public Object invoke(MethodInvocation methodInvocation) throws Throwable { + public @Nullable Object invoke(MethodInvocation methodInvocation) throws Throwable { if (requiresDecoration(methodInvocation.getMethod())) { Object target = methodInvocation.proceed(); + Assert.notNull(target, "invocation target was null"); if (target instanceof Proxy) { return target; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java index b52fc0bd71..5c50ba686a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java @@ -21,7 +21,7 @@ import java.util.Set; import java.util.stream.Collectors; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import com.mongodb.Function; @@ -41,10 +41,10 @@ class SimpleMongoTransactionOptions implements MongoTransactionOptions { static final Set KNOWN_KEYS = Arrays.stream(OptionKey.values()).map(OptionKey::getKey) .collect(Collectors.toSet()); - private final Duration maxCommitTime; - private final ReadConcern readConcern; - private final ReadPreference readPreference; - private final WriteConcern writeConcern; + private final @Nullable Duration maxCommitTime; + private final @Nullable ReadConcern readConcern; + private final @Nullable ReadPreference readPreference; + private final @Nullable WriteConcern writeConcern; static SimpleMongoTransactionOptions of(Map options) { return new SimpleMongoTransactionOptions(options); @@ -58,27 +58,23 @@ private SimpleMongoTransactionOptions(Map options) { this.writeConcern = doGetWriteConcern(options); } - @Nullable @Override - public Duration getMaxCommitTime() { + public @Nullable Duration getMaxCommitTime() { return maxCommitTime; } - @Nullable @Override - public ReadConcern getReadConcern() { + public @Nullable ReadConcern getReadConcern() { return readConcern; } - @Nullable @Override - public ReadPreference getReadPreference() { + public @Nullable ReadPreference getReadPreference() { return readPreference; } - @Nullable @Override - public WriteConcern getWriteConcern() { + public @Nullable WriteConcern getWriteConcern() { return writeConcern; } @@ -89,8 +85,7 @@ public String toString() { + ", readPreference=" + readPreference + ", writeConcern=" + writeConcern + '}'; } - @Nullable - private static Duration doGetMaxCommitTime(Map options) { + private static @Nullable Duration doGetMaxCommitTime(Map options) { return getValue(options, OptionKey.MAX_COMMIT_TIME, value -> { @@ -100,18 +95,15 @@ private static Duration doGetMaxCommitTime(Map options) { }); } - @Nullable - private static ReadConcern doGetReadConcern(Map options) { + private static @Nullable ReadConcern doGetReadConcern(Map options) { return getValue(options, OptionKey.READ_CONCERN, value -> new ReadConcern(ReadConcernLevel.fromString(value))); } - @Nullable - private static ReadPreference doGetReadPreference(Map options) { + private static @Nullable ReadPreference doGetReadPreference(Map options) { return getValue(options, OptionKey.READ_PREFERENCE, ReadPreference::valueOf); } - @Nullable - private static WriteConcern doGetWriteConcern(Map options) { + private static @Nullable WriteConcern doGetWriteConcern(Map options) { return getValue(options, OptionKey.WRITE_CONCERN, value -> { @@ -123,8 +115,8 @@ private static WriteConcern doGetWriteConcern(Map options) { }); } - @Nullable - private static T getValue(Map options, OptionKey key, Function convertFunction) { + private static @Nullable T getValue(Map options, OptionKey key, + Function convertFunction) { String value = options.get(key.getKey()); return value != null ? convertFunction.apply(value) : null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java index cd5f58d5b1..57ecec0342 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java @@ -17,7 +17,7 @@ import java.time.Duration; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * MongoDB-specific transaction metadata. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java index 37c7e3686b..e42c26d95a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.transaction.TransactionDefinition; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/UncategorizedMongoDbException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/UncategorizedMongoDbException.java index bec05d0d68..69ec086e5a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/UncategorizedMongoDbException.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/UncategorizedMongoDbException.java @@ -15,8 +15,8 @@ */ package org.springframework.data.mongodb; +import org.jspecify.annotations.Nullable; import org.springframework.dao.UncategorizedDataAccessException; -import org.springframework.lang.Nullable; public class UncategorizedMongoDbException extends UncategorizedDataAccessException { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java index 2fe27a2c9e..86a70600a8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java @@ -17,11 +17,11 @@ import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; import org.springframework.data.util.ReactiveWrappers; import org.springframework.data.util.ReactiveWrappers.ReactiveLibrary; import org.springframework.data.util.TypeUtils; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoManagedTypesBeanRegistrationAotProcessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoManagedTypesBeanRegistrationAotProcessor.java index a33f20ffb6..4b7aa10c3f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoManagedTypesBeanRegistrationAotProcessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoManagedTypesBeanRegistrationAotProcessor.java @@ -15,11 +15,11 @@ */ package org.springframework.data.mongodb.aot; +import org.jspecify.annotations.Nullable; import org.springframework.aot.generate.GenerationContext; import org.springframework.core.ResolvableType; import org.springframework.data.aot.ManagedTypesBeanRegistrationAotProcessor; import org.springframework.data.mongodb.MongoManagedTypes; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java index 538fe4e812..f2442960ed 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java @@ -15,10 +15,11 @@ */ package org.springframework.data.mongodb.aot; -import static org.springframework.data.mongodb.aot.MongoAotPredicates.*; +import static org.springframework.data.mongodb.aot.MongoAotPredicates.isReactorPresent; import java.util.Arrays; +import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -31,7 +32,6 @@ import org.springframework.data.mongodb.core.mapping.event.ReactiveAfterSaveCallback; import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeConvertCallback; import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeSaveCallback; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import com.mongodb.MongoClientSettings; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ConnectionStringPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ConnectionStringPropertyEditor.java index b070a0190f..0f6ba01704 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ConnectionStringPropertyEditor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ConnectionStringPropertyEditor.java @@ -17,7 +17,7 @@ import java.beans.PropertyEditorSupport; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.StringUtils; import com.mongodb.ConnectionString; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java index 164b4defb6..f3a7dc0437 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java @@ -21,6 +21,8 @@ import java.util.List; import java.util.Set; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; @@ -56,7 +58,6 @@ import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.event.ValidatingMongoEventListener; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -76,6 +77,7 @@ * @author Zied Yaich * @author Tomasz Forys */ +@NullUnmarked public class MappingMongoConverterParser implements BeanDefinitionParser { private static final String BASE_PACKAGE = "base-package"; @@ -157,8 +159,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { return null; } - @Nullable - private BeanDefinition potentiallyCreateValidatingMongoEventListener(Element element, ParserContext parserContext) { + private @Nullable BeanDefinition potentiallyCreateValidatingMongoEventListener(Element element, ParserContext parserContext) { String disableValidation = element.getAttribute("disable-validation"); boolean validationDisabled = StringUtils.hasText(disableValidation) && Boolean.parseBoolean(disableValidation); @@ -291,8 +292,7 @@ private static void parseFieldNamingStrategy(Element element, ReaderContext cont } } - @Nullable - private BeanDefinition getCustomConversions(Element element, ParserContext parserContext) { + private @Nullable BeanDefinition getCustomConversions(Element element, ParserContext parserContext) { List customConvertersElements = DomUtils.getChildElementsByTagName(element, "custom-converters"); @@ -354,8 +354,7 @@ private static Set getInitialEntityClasses(Element element) { return classes; } - @Nullable - public BeanMetadataElement parseConverter(Element element, ParserContext parserContext) { + public @Nullable BeanMetadataElement parseConverter(Element element, ParserContext parserContext) { String converterRef = element.getAttribute("ref"); if (StringUtils.hasText(converterRef)) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingBeanDefinitionParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingBeanDefinitionParser.java index 4e05fe6c39..a304199776 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingBeanDefinitionParser.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingBeanDefinitionParser.java @@ -18,6 +18,8 @@ import static org.springframework.data.config.ParsingUtils.*; import static org.springframework.data.mongodb.config.BeanNames.*; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -29,7 +31,6 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.event.AuditingEntityCallback; import org.springframework.data.mongodb.core.mapping.event.ReactiveAuditingEntityCallback; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -42,6 +43,7 @@ * @author Oliver Gierke * @author Mark Paluch */ +@NullUnmarked public class MongoAuditingBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { private static boolean PROJECT_REACTOR_AVAILABLE = ClassUtils.isPresent("reactor.core.publisher.Mono", diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java index 0594f6176c..b01827d8c6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java @@ -35,6 +35,7 @@ import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -52,7 +53,7 @@ public abstract class MongoConfigurationSupport { /** * Return the name of the database to connect to. * - * @return must not be {@literal null}. + * @return never {@literal null}. */ protected abstract String getDatabaseName(); @@ -76,7 +77,7 @@ protected Collection getMappingBasePackages() { * Creates a {@link MongoMappingContext} equipped with entity classes scanned from the mapping base package. * * @see #getMappingBasePackages() - * @return + * @return never {@literal null}. */ @Bean public MongoMappingContext mongoMappingContext(MongoCustomConversions customConversions, @@ -172,8 +173,10 @@ protected Set> scanForEntities(String basePackage) throws ClassNotFound for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) { - initialEntitySet - .add(ClassUtils.forName(candidate.getBeanClassName(), MongoConfigurationSupport.class.getClassLoader())); + String beanClassName = candidate.getBeanClassName(); + Assert.notNull(beanClassName, "BeanClassName cannot be null"); + + initialEntitySet.add(ClassUtils.forName(beanClassName, MongoConfigurationSupport.class.getClassLoader())); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoCredentialPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoCredentialPropertyEditor.java index b8f23a35af..93d778c861 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoCredentialPropertyEditor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoCredentialPropertyEditor.java @@ -26,7 +26,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoDbFactoryParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoDbFactoryParser.java index 2e733cc79f..2d3649c53a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoDbFactoryParser.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoDbFactoryParser.java @@ -20,6 +20,8 @@ import java.util.Set; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.parsing.BeanComponentDefinition; @@ -31,7 +33,6 @@ import org.springframework.data.config.BeanComponentDefinitionBuilder; import org.springframework.data.mongodb.core.MongoClientFactoryBean; import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import org.w3c.dom.Element; @@ -47,6 +48,7 @@ * @author Viktor Khoroshko * @author Mark Paluch */ +@NullUnmarked public class MongoDbFactoryParser extends AbstractBeanDefinitionParser { private static final Set MONGO_URI_ALLOWED_ADDITIONAL_ATTRIBUTES = Set.of("id", "write-concern"); @@ -125,8 +127,7 @@ private BeanDefinition registerMongoBeanDefinition(Element element, ParserContex * @param parserContext * @return {@literal null} in case no client-/uri defined. */ - @Nullable - private BeanDefinition getConnectionString(Element element, ParserContext parserContext) { + private @Nullable BeanDefinition getConnectionString(Element element, ParserContext parserContext) { String type = null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoJmxParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoJmxParser.java deleted file mode 100644 index af1ffbbb02..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoJmxParser.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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. - * 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.mongodb.config; - -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.parsing.BeanComponentDefinition; -import org.springframework.beans.factory.parsing.CompositeComponentDefinition; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.xml.BeanDefinitionParser; -import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.data.mongodb.core.MongoAdmin; -import org.springframework.data.mongodb.monitor.*; -import org.springframework.util.StringUtils; -import org.w3c.dom.Element; - -/** - * @author Mark Pollack - * @author Thomas Risberg - * @author John Brisbin - * @author Oliver Gierke - * @author Christoph Strobl - * @deprecated since 4.5 - */ -@Deprecated(since = "4.5", forRemoval = true) -public class MongoJmxParser implements BeanDefinitionParser { - - public BeanDefinition parse(Element element, ParserContext parserContext) { - String name = element.getAttribute("mongo-ref"); - if (!StringUtils.hasText(name)) { - name = BeanNames.MONGO_BEAN_NAME; - } - registerJmxComponents(name, element, parserContext); - return null; - } - - protected void registerJmxComponents(String mongoRefName, Element element, ParserContext parserContext) { - Object eleSource = parserContext.extractSource(element); - - CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), eleSource); - - createBeanDefEntry(AssertMetrics.class, compositeDef, mongoRefName, eleSource, parserContext); - createBeanDefEntry(BackgroundFlushingMetrics.class, compositeDef, mongoRefName, eleSource, parserContext); - createBeanDefEntry(BtreeIndexCounters.class, compositeDef, mongoRefName, eleSource, parserContext); - createBeanDefEntry(ConnectionMetrics.class, compositeDef, mongoRefName, eleSource, parserContext); - createBeanDefEntry(GlobalLockMetrics.class, compositeDef, mongoRefName, eleSource, parserContext); - createBeanDefEntry(MemoryMetrics.class, compositeDef, mongoRefName, eleSource, parserContext); - createBeanDefEntry(OperationCounters.class, compositeDef, mongoRefName, eleSource, parserContext); - createBeanDefEntry(ServerInfo.class, compositeDef, mongoRefName, eleSource, parserContext); - createBeanDefEntry(MongoAdmin.class, compositeDef, mongoRefName, eleSource, parserContext); - - parserContext.registerComponent(compositeDef); - - } - - protected void createBeanDefEntry(Class clazz, CompositeComponentDefinition compositeDef, String mongoRefName, - Object eleSource, ParserContext parserContext) { - BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz); - builder.getRawBeanDefinition().setSource(eleSource); - builder.addConstructorArgReference(mongoRefName); - BeanDefinition assertDef = builder.getBeanDefinition(); - String assertName = parserContext.getReaderContext().registerWithGeneratedName(assertDef); - compositeDef.addNestedComponent(new BeanComponentDefinition(assertDef, assertName)); - } - -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoNamespaceHandler.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoNamespaceHandler.java index 47519ca615..62a4a1082d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoNamespaceHandler.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoNamespaceHandler.java @@ -31,7 +31,6 @@ public void init() { registerBeanDefinitionParser("mapping-converter", new MappingMongoConverterParser()); registerBeanDefinitionParser("mongo-client", new MongoClientParser()); registerBeanDefinitionParser("db-factory", new MongoDbFactoryParser()); - registerBeanDefinitionParser("jmx", new MongoJmxParser()); registerBeanDefinitionParser("auditing", new MongoAuditingBeanDefinitionParser()); registerBeanDefinitionParser("template", new MongoTemplateParser()); registerBeanDefinitionParser("gridFsTemplate", new GridFsTemplateParser()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoParsingUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoParsingUtils.java index 95b56b58f3..00e993fdc8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoParsingUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoParsingUtils.java @@ -19,6 +19,7 @@ import java.util.Map; +import org.jspecify.annotations.NullUnmarked; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.CustomEditorConfigurer; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -40,6 +41,7 @@ * @author Christoph Strobl * @author Mark Paluch */ +@NullUnmarked abstract class MongoParsingUtils { private MongoParsingUtils() {} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoTemplateParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoTemplateParser.java index 1e1b11356f..5053e540fe 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoTemplateParser.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoTemplateParser.java @@ -18,6 +18,7 @@ import static org.springframework.data.config.ParsingUtils.*; import static org.springframework.data.mongodb.config.MongoParsingUtils.*; +import org.jspecify.annotations.NullUnmarked; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.parsing.BeanComponentDefinition; @@ -37,6 +38,7 @@ * @author Martin Baumgartner * @author Oliver Gierke */ +@NullUnmarked class MongoTemplateParser extends AbstractBeanDefinitionParser { @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadConcernPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadConcernPropertyEditor.java index 60bf126ae7..3f5cb0ca62 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadConcernPropertyEditor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadConcernPropertyEditor.java @@ -17,7 +17,7 @@ import java.beans.PropertyEditorSupport; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.StringUtils; import com.mongodb.ReadConcern; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadPreferencePropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadPreferencePropertyEditor.java index 5ed9b66619..f24c435348 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadPreferencePropertyEditor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadPreferencePropertyEditor.java @@ -17,10 +17,10 @@ import java.beans.PropertyEditorSupport; -import org.springframework.lang.Nullable; - import com.mongodb.ReadPreference; +import org.jspecify.annotations.Nullable; + /** * Parse a {@link String} to a {@link ReadPreference}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ServerAddressPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ServerAddressPropertyEditor.java index 9c51900902..9ff59e5b22 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ServerAddressPropertyEditor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ServerAddressPropertyEditor.java @@ -23,7 +23,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -80,11 +80,10 @@ public void setAsText(@Nullable String replicaSetString) { * @param source * @return the */ - @Nullable - private ServerAddress parseServerAddress(String source) { + private @Nullable ServerAddress parseServerAddress(String source) { if (!StringUtils.hasText(source)) { - if(LOG.isWarnEnabled()) { + if (LOG.isWarnEnabled()) { LOG.warn(String.format(COULD_NOT_PARSE_ADDRESS_MESSAGE, "source", source)); } return null; @@ -93,7 +92,7 @@ private ServerAddress parseServerAddress(String source) { String[] hostAndPort = extractHostAddressAndPort(source.trim()); if (hostAndPort.length > 2) { - if(LOG.isWarnEnabled()) { + if (LOG.isWarnEnabled()) { LOG.warn(String.format(COULD_NOT_PARSE_ADDRESS_MESSAGE, "source", source)); } return null; @@ -105,11 +104,11 @@ private ServerAddress parseServerAddress(String source) { return port == null ? new ServerAddress(hostAddress) : new ServerAddress(hostAddress, port); } catch (UnknownHostException e) { - if(LOG.isWarnEnabled()) { + if (LOG.isWarnEnabled()) { LOG.warn(String.format(COULD_NOT_PARSE_ADDRESS_MESSAGE, "host", hostAndPort[0])); } } catch (NumberFormatException e) { - if(LOG.isWarnEnabled()) { + if (LOG.isWarnEnabled()) { LOG.warn(String.format(COULD_NOT_PARSE_ADDRESS_MESSAGE, "port", hostAndPort[1])); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/UUidRepresentationPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/UUidRepresentationPropertyEditor.java index b777969967..23c15102ac 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/UUidRepresentationPropertyEditor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/UUidRepresentationPropertyEditor.java @@ -18,7 +18,7 @@ import java.beans.PropertyEditorSupport; import org.bson.UuidRepresentation; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.StringUtils; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/WriteConcernPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/WriteConcernPropertyEditor.java index ee0d09e555..32c19e24c3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/WriteConcernPropertyEditor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/WriteConcernPropertyEditor.java @@ -17,7 +17,7 @@ import java.beans.PropertyEditorSupport; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.StringUtils; import com.mongodb.WriteConcern; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/package-info.java index 5a1e5b725e..555cc9f66e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/package-info.java @@ -1,6 +1,6 @@ /** * Spring XML namespace configuration for MongoDB specific repositories. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.config; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/AggregationUtil.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/AggregationUtil.java index a00d95a9ad..ec7c368eaf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/AggregationUtil.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/AggregationUtil.java @@ -18,7 +18,7 @@ import java.util.List; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext; @@ -30,7 +30,6 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; /** * Utility methods to map {@link org.springframework.data.mongodb.core.aggregation.Aggregation} pipeline definitions and diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamEvent.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamEvent.java index 17b8835b7e..8a74ace28b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamEvent.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamEvent.java @@ -21,9 +21,9 @@ import org.bson.BsonTimestamp; import org.bson.BsonValue; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.messaging.Message; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -78,8 +78,7 @@ public ChangeStreamEvent(@Nullable ChangeStreamDocument raw, Class * * @return can be {@literal null}. */ - @Nullable - public ChangeStreamDocument getRaw() { + public @Nullable ChangeStreamDocument getRaw() { return raw; } @@ -88,10 +87,10 @@ public ChangeStreamDocument getRaw() { * * @return can be {@literal null}. */ - @Nullable - public Instant getTimestamp() { + public @Nullable Instant getTimestamp() { - return getBsonTimestamp() != null ? converter.getConversionService().convert(raw.getClusterTime(), Instant.class) + return getBsonTimestamp() != null && raw != null + ? converter.getConversionService().convert(raw.getClusterTime(), Instant.class) : null; } @@ -111,8 +110,7 @@ public BsonTimestamp getBsonTimestamp() { * * @return can be {@literal null}. */ - @Nullable - public BsonValue getResumeToken() { + public @Nullable BsonValue getResumeToken() { return raw != null ? raw.getResumeToken() : null; } @@ -121,8 +119,7 @@ public BsonValue getResumeToken() { * * @return can be {@literal null}. */ - @Nullable - public OperationType getOperationType() { + public @Nullable OperationType getOperationType() { return raw != null ? raw.getOperationType() : null; } @@ -131,8 +128,7 @@ public OperationType getOperationType() { * * @return can be {@literal null}. */ - @Nullable - public String getDatabaseName() { + public @Nullable String getDatabaseName() { return raw != null ? raw.getNamespace().getDatabaseName() : null; } @@ -141,8 +137,7 @@ public String getDatabaseName() { * * @return can be {@literal null}. */ - @Nullable - public String getCollectionName() { + public @Nullable String getCollectionName() { return raw != null ? raw.getNamespace().getCollectionName() : null; } @@ -152,8 +147,7 @@ public String getCollectionName() { * @return {@literal null} when {@link #getRaw()} or {@link ChangeStreamDocument#getFullDocument()} is * {@literal null}. */ - @Nullable - public T getBody() { + public @Nullable T getBody() { if (raw == null || raw.getFullDocument() == null) { return null; @@ -163,14 +157,14 @@ public T getBody() { } /** - * Get the potentially converted {@link ChangeStreamDocument#getFullDocumentBeforeChange() document} before being changed. + * Get the potentially converted {@link ChangeStreamDocument#getFullDocumentBeforeChange() document} before being + * changed. * * @return {@literal null} when {@link #getRaw()} or {@link ChangeStreamDocument#getFullDocumentBeforeChange()} is * {@literal null}. * @since 4.0 */ - @Nullable - public T getBodyBeforeChange() { + public @Nullable T getBodyBeforeChange() { if (raw == null || raw.getFullDocumentBeforeChange() == null) { return null; @@ -189,6 +183,7 @@ private T getConvertedFullDocument(Document fullDocument) { return (T) doGetConverted(fullDocument, CONVERTED_FULL_DOCUMENT_UPDATER); } + @SuppressWarnings("NullAway") private Object doGetConverted(Document fullDocument, AtomicReferenceFieldUpdater updater) { Object result = updater.get(this); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamOptions.java index aaee3b76af..9c99b0e01f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamOptions.java @@ -23,9 +23,10 @@ import org.bson.BsonTimestamp; import org.bson.BsonValue; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.query.Collation; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -248,6 +249,7 @@ private ChangeStreamOptionsBuilder() {} * @param collation must not be {@literal null} nor {@literal empty}. * @return this. */ + @Contract("_ -> this") public ChangeStreamOptionsBuilder collation(Collation collation) { Assert.notNull(collation, "Collation must not be null nor empty"); @@ -257,14 +259,12 @@ public ChangeStreamOptionsBuilder collation(Collation collation) { } /** - * Set the filter to apply. - *
+ * Set the filter to apply.
* Fields on aggregation expression root level are prefixed to map to fields contained in * {@link ChangeStreamDocument#getFullDocument() fullDocument}. However {@literal operationType}, {@literal ns}, * {@literal documentKey} and {@literal fullDocument} are reserved words that will be omitted, and therefore taken * as given, during the mapping procedure. You may want to have a look at the - * structure of Change Events. - *
+ * structure of Change Events.
* Use {@link org.springframework.data.mongodb.core.aggregation.TypedAggregation} to ensure filter expressions are * mapped to domain type fields. * @@ -272,6 +272,7 @@ public ChangeStreamOptionsBuilder collation(Collation collation) { * {@literal null}. * @return this. */ + @Contract("_ -> this") public ChangeStreamOptionsBuilder filter(Aggregation filter) { Assert.notNull(filter, "Filter must not be null"); @@ -286,6 +287,7 @@ public ChangeStreamOptionsBuilder filter(Aggregation filter) { * @param filter must not be {@literal null} nor contain {@literal null} values. * @return this. */ + @Contract("_ -> this") public ChangeStreamOptionsBuilder filter(Document... filter) { Assert.noNullElements(filter, "Filter must not contain null values"); @@ -301,6 +303,7 @@ public ChangeStreamOptionsBuilder filter(Document... filter) { * @param resumeToken must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public ChangeStreamOptionsBuilder resumeToken(BsonValue resumeToken) { Assert.notNull(resumeToken, "ResumeToken must not be null"); @@ -330,6 +333,7 @@ public ChangeStreamOptionsBuilder returnFullDocumentOnUpdate() { * @param lookup must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public ChangeStreamOptionsBuilder fullDocumentLookup(FullDocument lookup) { Assert.notNull(lookup, "Lookup must not be null"); @@ -345,6 +349,7 @@ public ChangeStreamOptionsBuilder fullDocumentLookup(FullDocument lookup) { * @return this. * @since 4.0 */ + @Contract("_ -> this") public ChangeStreamOptionsBuilder fullDocumentBeforeChangeLookup(FullDocumentBeforeChange lookup) { Assert.notNull(lookup, "Lookup must not be null"); @@ -358,7 +363,7 @@ public ChangeStreamOptionsBuilder fullDocumentBeforeChangeLookup(FullDocumentBef * * @return this. * @since 4.0 - * @see #fullDocumentBeforeChangeLookup(FullDocumentBeforeChange) + * @see #fullDocumentBeforeChangeLookup(FullDocumentBeforeChange) */ public ChangeStreamOptionsBuilder returnFullDocumentBeforeChange() { return fullDocumentBeforeChangeLookup(FullDocumentBeforeChange.WHEN_AVAILABLE); @@ -370,6 +375,7 @@ public ChangeStreamOptionsBuilder returnFullDocumentBeforeChange() { * @param resumeTimestamp must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public ChangeStreamOptionsBuilder resumeAt(Instant resumeTimestamp) { Assert.notNull(resumeTimestamp, "ResumeTimestamp must not be null"); @@ -385,6 +391,7 @@ public ChangeStreamOptionsBuilder resumeAt(Instant resumeTimestamp) { * @return this. * @since 2.2 */ + @Contract("_ -> this") public ChangeStreamOptionsBuilder resumeAt(BsonTimestamp resumeTimestamp) { Assert.notNull(resumeTimestamp, "ResumeTimestamp must not be null"); @@ -400,6 +407,7 @@ public ChangeStreamOptionsBuilder resumeAt(BsonTimestamp resumeTimestamp) { * @return this. * @since 2.2 */ + @Contract("_ -> this") public ChangeStreamOptionsBuilder resumeAfter(BsonValue resumeToken) { resumeToken(resumeToken); @@ -415,6 +423,7 @@ public ChangeStreamOptionsBuilder resumeAfter(BsonValue resumeToken) { * @return this. * @since 2.2 */ + @Contract("_ -> this") public ChangeStreamOptionsBuilder startAfter(BsonValue resumeToken) { resumeToken(resumeToken); @@ -426,6 +435,7 @@ public ChangeStreamOptionsBuilder startAfter(BsonValue resumeToken) { /** * @return the built {@link ChangeStreamOptions} */ + @Contract("-> new") public ChangeStreamOptions build() { ChangeStreamOptions options = new ChangeStreamOptions(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionCallback.java index c142aca173..bf8be5ba69 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionCallback.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionCallback.java @@ -16,8 +16,8 @@ package org.springframework.data.mongodb.core; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessException; -import org.springframework.lang.Nullable; import com.mongodb.MongoException; import com.mongodb.client.MongoCollection; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java index d627ba2468..f4d1891703 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java @@ -15,18 +15,35 @@ */ package org.springframework.data.mongodb.core; +import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.function.Function; +import java.util.stream.StreamSupport; +import org.bson.BsonBinary; +import org.bson.BsonBinarySubType; +import org.bson.BsonNull; +import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.QueryableJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.JsonSchemaProperty; import org.springframework.data.mongodb.core.schema.MongoJsonSchema; +import org.springframework.data.mongodb.core.schema.QueryCharacteristic; import org.springframework.data.mongodb.core.timeseries.Granularity; import org.springframework.data.mongodb.core.timeseries.GranularityDefinition; import org.springframework.data.mongodb.core.validation.Validator; import org.springframework.data.util.Optionals; -import org.springframework.lang.Nullable; +import org.springframework.lang.CheckReturnValue; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -41,6 +58,7 @@ * @author Mark Paluch * @author Andreas Zink * @author Ben Foster + * @author Ross Lawley */ public class CollectionOptions { @@ -51,10 +69,12 @@ public class CollectionOptions { private ValidationOptions validationOptions; private @Nullable TimeSeriesOptions timeSeriesOptions; private @Nullable CollectionChangeStreamOptions changeStreamOptions; + private @Nullable EncryptedFieldsOptions encryptedFieldsOptions; private CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nullable Boolean capped, @Nullable Collation collation, ValidationOptions validationOptions, @Nullable TimeSeriesOptions timeSeriesOptions, - @Nullable CollectionChangeStreamOptions changeStreamOptions) { + @Nullable CollectionChangeStreamOptions changeStreamOptions, + @Nullable EncryptedFieldsOptions encryptedFieldsOptions) { this.maxDocuments = maxDocuments; this.size = size; @@ -63,6 +83,7 @@ private CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nul this.validationOptions = validationOptions; this.timeSeriesOptions = timeSeriesOptions; this.changeStreamOptions = changeStreamOptions; + this.encryptedFieldsOptions = encryptedFieldsOptions; } /** @@ -76,7 +97,7 @@ public static CollectionOptions just(Collation collation) { Assert.notNull(collation, "Collation must not be null"); - return new CollectionOptions(null, null, null, collation, ValidationOptions.none(), null, null); + return new CollectionOptions(null, null, null, collation, ValidationOptions.none(), null, null, null); } /** @@ -86,7 +107,7 @@ public static CollectionOptions just(Collation collation) { * @since 2.0 */ public static CollectionOptions empty() { - return new CollectionOptions(null, null, null, null, ValidationOptions.none(), null, null); + return new CollectionOptions(null, null, null, null, ValidationOptions.none(), null, null, null); } /** @@ -127,6 +148,46 @@ public static CollectionOptions emitChangedRevisions() { return empty().changeStream(CollectionChangeStreamOptions.preAndPostImages(true)); } + /** + * Create new {@link CollectionOptions} with the given {@code encryptedFields}. + * + * @param encryptedFieldsOptions can be null + * @return new instance of {@link CollectionOptions}. + * @since 4.5.0 + */ + @Contract("_ -> new") + @CheckReturnValue + public static CollectionOptions encryptedCollection(@Nullable EncryptedFieldsOptions encryptedFieldsOptions) { + return new CollectionOptions(null, null, null, null, ValidationOptions.NONE, null, null, encryptedFieldsOptions); + } + + /** + * Create new {@link CollectionOptions} reading encryption options from the given {@link MongoJsonSchema}. + * + * @param schema must not be {@literal null}. + * @return new instance of {@link CollectionOptions}. + * @since 4.5.0 + */ + @Contract("_ -> new") + @CheckReturnValue + public static CollectionOptions encryptedCollection(MongoJsonSchema schema) { + return encryptedCollection(EncryptedFieldsOptions.fromSchema(schema)); + } + + /** + * Create new {@link CollectionOptions} building encryption options in a fluent style. + * + * @param optionsFunction must not be {@literal null}. + * @return new instance of {@link CollectionOptions}. + * @since 4.5.0 + */ + @Contract("_ -> new") + @CheckReturnValue + public static CollectionOptions encryptedCollection( + Function optionsFunction) { + return encryptedCollection(optionsFunction.apply(new EncryptedFieldsOptions())); + } + /** * Create new {@link CollectionOptions} with already given settings and capped set to {@literal true}.
* NOTE: Using capped collections requires defining {@link #size(long)}. @@ -136,7 +197,7 @@ public static CollectionOptions emitChangedRevisions() { */ public CollectionOptions capped() { return new CollectionOptions(size, maxDocuments, true, collation, validationOptions, timeSeriesOptions, - changeStreamOptions); + changeStreamOptions, encryptedFieldsOptions); } /** @@ -148,7 +209,7 @@ public CollectionOptions capped() { */ public CollectionOptions maxDocuments(long maxDocuments) { return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, - changeStreamOptions); + changeStreamOptions, encryptedFieldsOptions); } /** @@ -160,7 +221,7 @@ public CollectionOptions maxDocuments(long maxDocuments) { */ public CollectionOptions size(long size) { return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, - changeStreamOptions); + changeStreamOptions, encryptedFieldsOptions); } /** @@ -172,19 +233,19 @@ public CollectionOptions size(long size) { */ public CollectionOptions collation(@Nullable Collation collation) { return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, - changeStreamOptions); + changeStreamOptions, encryptedFieldsOptions); } /** * Create new {@link CollectionOptions} with already given settings and {@code validationOptions} set to given * {@link MongoJsonSchema}. * - * @param schema can be {@literal null}. + * @param schema must not be {@literal null}. * @return new {@link CollectionOptions}. * @since 2.1 */ - public CollectionOptions schema(@Nullable MongoJsonSchema schema) { - return validator(Validator.schema(schema)); + public CollectionOptions schema(MongoJsonSchema schema) { + return validator(schema != null ? Validator.schema(schema) : null); } /** @@ -293,7 +354,7 @@ public CollectionOptions validation(ValidationOptions validationOptions) { Assert.notNull(validationOptions, "ValidationOptions must not be null"); return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, - changeStreamOptions); + changeStreamOptions, encryptedFieldsOptions); } /** @@ -307,7 +368,7 @@ public CollectionOptions timeSeries(TimeSeriesOptions timeSeriesOptions) { Assert.notNull(timeSeriesOptions, "TimeSeriesOptions must not be null"); return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, - changeStreamOptions); + changeStreamOptions, encryptedFieldsOptions); } /** @@ -321,7 +382,22 @@ public CollectionOptions changeStream(CollectionChangeStreamOptions changeStream Assert.notNull(changeStreamOptions, "ChangeStreamOptions must not be null"); return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, - changeStreamOptions); + changeStreamOptions, encryptedFieldsOptions); + } + + /** + * Set the {@link EncryptedFieldsOptions} for collections using queryable encryption. + * + * @param encryptedFieldsOptions must not be {@literal null}. + * @return new instance of {@link CollectionOptions}. + */ + @Contract("_ -> new") + @CheckReturnValue + public CollectionOptions encrypted(EncryptedFieldsOptions encryptedFieldsOptions) { + + Assert.notNull(encryptedFieldsOptions, "EncryptedCollectionOptions must not be null"); + return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, + changeStreamOptions, encryptedFieldsOptions); } /** @@ -392,14 +468,24 @@ public Optional getChangeStreamOptions() { return Optional.ofNullable(changeStreamOptions); } + /** + * Get the {@code encryptedFields} if available. + * + * @return {@link Optional#empty()} if not specified. + * @since 4.5 + */ + public Optional getEncryptedFieldsOptions() { + return Optional.ofNullable(encryptedFieldsOptions); + } + @Override public String toString() { return "CollectionOptions{" + "maxDocuments=" + maxDocuments + ", size=" + size + ", capped=" + capped + ", collation=" + collation + ", validationOptions=" + validationOptions + ", timeSeriesOptions=" - + timeSeriesOptions + ", changeStreamOptions=" + changeStreamOptions + ", disableValidation=" - + disableValidation() + ", strictValidation=" + strictValidation() + ", moderateValidation=" - + moderateValidation() + ", warnOnValidationError=" + warnOnValidationError() + ", failOnValidationError=" - + failOnValidationError() + '}'; + + timeSeriesOptions + ", changeStreamOptions=" + changeStreamOptions + ", encryptedCollectionOptions=" + + encryptedFieldsOptions + ", disableValidation=" + disableValidation() + ", strictValidation=" + + strictValidation() + ", moderateValidation=" + moderateValidation() + ", warnOnValidationError=" + + warnOnValidationError() + ", failOnValidationError=" + failOnValidationError() + '}'; } @Override @@ -431,7 +517,10 @@ public boolean equals(@Nullable Object o) { if (!ObjectUtils.nullSafeEquals(timeSeriesOptions, that.timeSeriesOptions)) { return false; } - return ObjectUtils.nullSafeEquals(changeStreamOptions, that.changeStreamOptions); + if (!ObjectUtils.nullSafeEquals(changeStreamOptions, that.changeStreamOptions)) { + return false; + } + return ObjectUtils.nullSafeEquals(encryptedFieldsOptions, that.encryptedFieldsOptions); } @Override @@ -443,6 +532,7 @@ public int hashCode() { result = 31 * result + ObjectUtils.nullSafeHashCode(validationOptions); result = 31 * result + ObjectUtils.nullSafeHashCode(timeSeriesOptions); result = 31 * result + ObjectUtils.nullSafeHashCode(changeStreamOptions); + result = 31 * result + ObjectUtils.nullSafeHashCode(encryptedFieldsOptions); return result; } @@ -461,7 +551,8 @@ public static class ValidationOptions { private final @Nullable ValidationLevel validationLevel; private final @Nullable ValidationAction validationAction; - public ValidationOptions(Validator validator, ValidationLevel validationLevel, ValidationAction validationAction) { + public ValidationOptions(@Nullable Validator validator, @Nullable ValidationLevel validationLevel, + @Nullable ValidationAction validationAction) { this.validator = validator; this.validationLevel = validationLevel; @@ -483,6 +574,7 @@ public static ValidationOptions none() { * @param validator can be {@literal null}. * @return new instance of {@link ValidationOptions}. */ + @Contract("_ -> new") public ValidationOptions validator(@Nullable Validator validator) { return new ValidationOptions(validator, validationLevel, validationAction); } @@ -493,6 +585,7 @@ public ValidationOptions validator(@Nullable Validator validator) { * @param validationLevel can be {@literal null}. * @return new instance of {@link ValidationOptions}. */ + @Contract("_ -> new") public ValidationOptions validationLevel(ValidationLevel validationLevel) { return new ValidationOptions(validator, validationLevel, validationAction); } @@ -503,6 +596,7 @@ public ValidationOptions validationLevel(ValidationLevel validationLevel) { * @param validationAction can be {@literal null}. * @return new instance of {@link ValidationOptions}. */ + @Contract("_ -> new") public ValidationOptions validationAction(ValidationAction validationAction) { return new ValidationOptions(validator, validationLevel, validationAction); } @@ -576,6 +670,188 @@ public int hashCode() { } } + /** + * Encapsulation of Encryption options for collections. + * + * @author Christoph Strobl + * @since 4.5 + */ + public static class EncryptedFieldsOptions { + + private static final EncryptedFieldsOptions NONE = new EncryptedFieldsOptions(); + + private final @Nullable MongoJsonSchema schema; + private final List queryableProperties; + + EncryptedFieldsOptions() { + this(null, List.of()); + } + + private EncryptedFieldsOptions(@Nullable MongoJsonSchema schema, + List queryableProperties) { + + this.schema = schema; + this.queryableProperties = queryableProperties; + } + + /** + * @return {@link EncryptedFieldsOptions#NONE} + */ + public static EncryptedFieldsOptions none() { + return NONE; + } + + /** + * @return new instance of {@link EncryptedFieldsOptions}. + */ + public static EncryptedFieldsOptions fromSchema(MongoJsonSchema schema) { + return new EncryptedFieldsOptions(schema, List.of()); + } + + /** + * @return new instance of {@link EncryptedFieldsOptions}. + */ + public static EncryptedFieldsOptions fromProperties(List properties) { + return new EncryptedFieldsOptions(null, List.copyOf(properties)); + } + + /** + * Add a new {@link QueryableJsonSchemaProperty queryable property} for the given source property. + *

+ * Please note that, a given {@link JsonSchemaProperty} may override options from a given {@link MongoJsonSchema} if + * set. + * + * @param property the queryable source - typically + * {@link org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty + * encrypted}. + * @param characteristics the query options to set. + * @return new instance of {@link EncryptedFieldsOptions}. + */ + @Contract("_, _ -> new") + @CheckReturnValue + public EncryptedFieldsOptions queryable(JsonSchemaProperty property, QueryCharacteristic... characteristics) { + + List targetPropertyList = new ArrayList<>(queryableProperties.size() + 1); + targetPropertyList.addAll(queryableProperties); + targetPropertyList.add(JsonSchemaProperty.queryable(property, List.of(characteristics))); + + return new EncryptedFieldsOptions(schema, targetPropertyList); + } + + public Document toDocument() { + return new Document("fields", selectPaths()); + } + + private List selectPaths() { + + Map fields = new LinkedHashMap<>(); + for (Document field : fromSchema()) { + fields.put(field.get("path", String.class), field); + } + for (Document field : fromProperties()) { + fields.put(field.get("path", String.class), field); + } + return List.copyOf(fields.values()); + } + + private List fromProperties() { + + if (queryableProperties.isEmpty()) { + return List.of(); + } + + List converted = new ArrayList<>(queryableProperties.size()); + for (QueryableJsonSchemaProperty property : queryableProperties) { + + Document field = new Document("path", property.getIdentifier()); + + if (!property.getTypes().isEmpty()) { + field.append("bsonType", property.getTypes().iterator().next().toBsonType().value()); + } + + if (property + .getTargetProperty() instanceof IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty encrypted) { + if (encrypted.getKeyId() != null) { + if (encrypted.getKeyId() instanceof String stringKey) { + field.append("keyId", + new BsonBinary(BsonBinarySubType.UUID_STANDARD, stringKey.getBytes(StandardCharsets.UTF_8))); + } else { + field.append("keyId", encrypted.getKeyId()); + } + } + } + + field.append("queries", StreamSupport.stream(property.getCharacteristics().spliterator(), false) + .map(QueryCharacteristic::toDocument).toList()); + + if (!field.containsKey("keyId")) { + field.append("keyId", BsonNull.VALUE); + } + + converted.add(field); + } + return converted; + } + + private List fromSchema() { + + if (schema == null) { + return List.of(); + } + + Document root = schema.schemaDocument(); + Map paths = new LinkedHashMap<>(); + collectPaths(root, null, paths); + + List fields = new ArrayList<>(); + if (!paths.isEmpty()) { + + for (Entry entry : paths.entrySet()) { + Document field = new Document("path", entry.getKey()); + field.append("keyId", entry.getValue().getOrDefault("keyId", BsonNull.VALUE)); + if (entry.getValue().containsKey("bsonType")) { + field.append("bsonType", entry.getValue().get("bsonType")); + } + field.put("queries", entry.getValue().get("queries")); + fields.add(field); + } + } + + return fields; + } + } + + private static void collectPaths(Document document, @Nullable String currentPath, Map paths) { + + if (document.containsKey("type") && document.get("type").equals("object")) { + Object o = document.get("properties"); + if (o == null) { + return; + } + + if (o instanceof Document properties) { + for (Entry entry : properties.entrySet()) { + if (entry.getValue() instanceof Document nested) { + + String path = currentPath == null ? entry.getKey() : (currentPath + "." + entry.getKey()); + if (nested.containsKey("encrypt")) { + Document target = new Document(nested.get("encrypt", Document.class)); + if (nested.containsKey("queries")) { + List queries = nested.get("queries", List.class); + if (!queries.isEmpty() && queries.iterator().next() instanceof Document qd) { + target.putAll(qd); + } + } + paths.put(path, target); + } else { + collectPaths(nested, path, paths); + } + } + } + } + } + } + /** * Encapsulation of options applied to define collections change stream behaviour. * @@ -677,6 +953,7 @@ public static TimeSeriesOptions timeSeries(String timeField) { * @param metaField must not be {@literal null}. * @return new instance of {@link TimeSeriesOptions}. */ + @Contract("_ -> new") public TimeSeriesOptions metaField(String metaField) { return new TimeSeriesOptions(timeField, metaField, granularity, expireAfter); } @@ -688,6 +965,7 @@ public TimeSeriesOptions metaField(String metaField) { * @return new instance of {@link TimeSeriesOptions}. * @see Granularity */ + @Contract("_ -> new") public TimeSeriesOptions granularity(GranularityDefinition granularity) { return new TimeSeriesOptions(timeField, metaField, granularity, expireAfter); } @@ -700,6 +978,7 @@ public TimeSeriesOptions granularity(GranularityDefinition granularity) { * @see com.mongodb.client.model.CreateCollectionOptions#expireAfter(long, java.util.concurrent.TimeUnit) * @since 4.4 */ + @Contract("_ -> new") public TimeSeriesOptions expireAfter(Duration ttl) { return new TimeSeriesOptions(timeField, metaField, granularity, ttl); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionPreparerSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionPreparerSupport.java index 644a3a54d1..bdf0b90ee3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionPreparerSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionPreparerSupport.java @@ -21,6 +21,7 @@ import java.util.function.Function; import org.bson.Document; +import org.jspecify.annotations.Nullable; import com.mongodb.ReadConcern; import com.mongodb.ReadPreference; @@ -84,7 +85,7 @@ public boolean hasReadConcern() { } @Override - public ReadConcern getReadConcern() { + public @Nullable ReadConcern getReadConcern() { for (Object aware : sources) { if (aware instanceof ReadConcernAware rca && rca.hasReadConcern()) { @@ -108,7 +109,7 @@ public boolean hasReadPreference() { } @Override - public ReadPreference getReadPreference() { + public @Nullable ReadPreference getReadPreference() { for (Object aware : sources) { if (aware instanceof ReadPreferenceAware rpa && rpa.hasReadPreference()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CountQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CountQuery.java index 4fa6b3e97d..11d9f09afd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CountQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CountQuery.java @@ -23,9 +23,9 @@ import java.util.Map; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.query.MetricConversion; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -154,7 +154,7 @@ private Collection rewriteCollection(Collection source) { * @param $and potentially existing {@code $and} condition. * @return the rewritten query {@link Document}. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "NullAway" }) private static Document createGeoWithin(String key, Document source, @Nullable Object $and) { boolean spheric = source.containsKey("$nearSphere"); @@ -233,6 +233,7 @@ private static boolean containsNearWithMinDistance(Document source) { return source.containsKey("$minDistance"); } + @SuppressWarnings("NullAway") private static Object toCenterCoordinates(Object value) { if (ObjectUtils.isArray(value)) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CursorPreparer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CursorPreparer.java index 9b7408b0cf..3b53cef8d0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CursorPreparer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CursorPreparer.java @@ -18,7 +18,7 @@ import java.util.function.Function; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import com.mongodb.ReadPreference; @@ -76,8 +76,7 @@ default FindIterable initiateFind(MongoCollection collection * @since 2.2 */ @Override - @Nullable - default ReadPreference getReadPreference() { + default @Nullable ReadPreference getReadPreference() { return null; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DbCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DbCallback.java index 9d588ad16d..f450bddb30 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DbCallback.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DbCallback.java @@ -15,8 +15,8 @@ */ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessException; -import org.springframework.lang.Nullable; import com.mongodb.MongoException; import com.mongodb.client.MongoDatabase; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java index 52343522a7..8bc5349e61 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java @@ -21,6 +21,7 @@ import java.util.stream.Collectors; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.dao.DataIntegrityViolationException; @@ -40,7 +41,7 @@ import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.util.Pair; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import com.mongodb.MongoBulkWriteException; @@ -115,6 +116,7 @@ void setDefaultWriteConcern(@Nullable WriteConcern defaultWriteConcern) { } @Override + @Contract("_ -> this") public BulkOperations insert(Object document) { Assert.notNull(document, "Document must not be null"); @@ -127,6 +129,7 @@ public BulkOperations insert(Object document) { } @Override + @Contract("_ -> this") public BulkOperations insert(List documents) { Assert.notNull(documents, "Documents must not be null"); @@ -137,6 +140,7 @@ public BulkOperations insert(List documents) { } @Override + @Contract("_, _ -> this") public BulkOperations updateOne(Query query, UpdateDefinition update) { Assert.notNull(query, "Query must not be null"); @@ -146,6 +150,7 @@ public BulkOperations updateOne(Query query, UpdateDefinition update) { } @Override + @Contract("_ -> this") public BulkOperations updateOne(List> updates) { Assert.notNull(updates, "Updates must not be null"); @@ -158,6 +163,7 @@ public BulkOperations updateOne(List> updates) { } @Override + @Contract("_, _ -> this") public BulkOperations updateMulti(Query query, UpdateDefinition update) { Assert.notNull(query, "Query must not be null"); @@ -169,6 +175,7 @@ public BulkOperations updateMulti(Query query, UpdateDefinition update) { } @Override + @Contract("_ -> this") public BulkOperations updateMulti(List> updates) { Assert.notNull(updates, "Updates must not be null"); @@ -181,11 +188,13 @@ public BulkOperations updateMulti(List> updates) { } @Override + @Contract("_, _ -> this") public BulkOperations upsert(Query query, UpdateDefinition update) { return update(query, update, true, true); } @Override + @Contract("_ -> this") public BulkOperations upsert(List> updates) { for (Pair update : updates) { @@ -196,6 +205,7 @@ public BulkOperations upsert(List> updates) { } @Override + @Contract("_ -> this") public BulkOperations remove(Query query) { Assert.notNull(query, "Query must not be null"); @@ -209,6 +219,7 @@ public BulkOperations remove(Query query) { } @Override + @Contract("_ -> this") public BulkOperations remove(List removes) { Assert.notNull(removes, "Removals must not be null"); @@ -221,6 +232,7 @@ public BulkOperations remove(List removes) { } @Override + @Contract("_, _, _ -> this") public BulkOperations replaceOne(Query query, Object replacement, FindAndReplaceOptions options) { Assert.notNull(query, "Query must not be null"); @@ -412,7 +424,7 @@ public boolean skipEventPublishing() { return eventPublisher == null; } - @SuppressWarnings("rawtypes") + @SuppressWarnings({ "rawtypes", "NullAway" }) public T callback(Class callbackType, T entity, String collectionName) { if (skipEntityCallbacks()) { @@ -422,7 +434,7 @@ public T callback(Class callbackType, T entity, St return entityCallbacks.callback(callbackType, entity, collectionName); } - @SuppressWarnings("rawtypes") + @SuppressWarnings({ "rawtypes", "NullAway" }) public T callback(Class callbackType, T entity, Document document, String collectionName) { @@ -433,6 +445,7 @@ public T callback(Class callbackType, T entity, Do return entityCallbacks.callback(callbackType, entity, document, collectionName); } + @SuppressWarnings("NullAway") public void publishEvent(ApplicationEvent event) { if (skipEventPublishing()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java index 2057e2f046..24d22bd80a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java @@ -20,6 +20,7 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessException; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.UncategorizedMongoDbException; @@ -28,7 +29,6 @@ import org.springframework.data.mongodb.core.index.IndexInfo; import org.springframework.data.mongodb.core.index.IndexOperations; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.NumberUtils; @@ -115,6 +115,7 @@ public DefaultIndexOperations(MongoOperations mongoOperations, String collection } @Override + @SuppressWarnings("NullAway") public String ensureIndex(IndexDefinition indexDefinition) { return execute(collection -> { @@ -131,8 +132,7 @@ public String ensureIndex(IndexDefinition indexDefinition) { }); } - @Nullable - private MongoPersistentEntity lookupPersistentEntity(@Nullable Class entityType, String collection) { + private @Nullable MongoPersistentEntity lookupPersistentEntity(@Nullable Class entityType, String collection) { if (entityType != null) { return mapper.getMappingContext().getRequiredPersistentEntity(entityType); @@ -160,6 +160,7 @@ public void dropIndex(String name) { } @Override + @SuppressWarnings("NullAway") public void alterIndex(String name, org.springframework.data.mongodb.core.index.IndexOptions options) { Document indexOptions = new Document("name", name); @@ -180,6 +181,7 @@ public void dropAllIndexes() { } @Override + @SuppressWarnings("NullAway") public List getIndexInfo() { return execute(new CollectionCallback>() { @@ -208,8 +210,7 @@ private List getIndexData(MongoCursor cursor) { }); } - @Nullable - public T execute(CollectionCallback callback) { + public @Nullable T execute(CollectionCallback callback) { Assert.notNull(callback, "CollectionCallback must not be null"); @@ -228,6 +229,7 @@ private IndexOptions addPartialFilterIfPresent(IndexOptions ops, Document source mapper.getMappedSort((Document) sourceOptions.get(PARTIAL_FILTER_EXPRESSION_KEY), entity)); } + @SuppressWarnings("NullAway") private static IndexOptions addDefaultCollationIfRequired(IndexOptions ops, @Nullable MongoPersistentEntity entity) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java index e2471dbb14..a34c1fb945 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.index.IndexOperations; @@ -43,7 +44,7 @@ class DefaultIndexOperationsProvider implements IndexOperationsProvider { } @Override - public IndexOperations indexOps(String collectionName, Class type) { + public IndexOperations indexOps(String collectionName, @Nullable Class type) { return new DefaultIndexOperations(mongoDbFactory, collectionName, mapper, type); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperations.java index 59b7ccd63e..92c6a957dc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperations.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core; +import org.springframework.lang.Contract; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -24,6 +25,7 @@ import java.util.stream.Collectors; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.mapping.callback.EntityCallback; @@ -40,7 +42,6 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.mongodb.WriteConcern; @@ -107,6 +108,7 @@ void setDefaultWriteConcern(@Nullable WriteConcern defaultWriteConcern) { } @Override + @Contract("_ -> this") public ReactiveBulkOperations insert(Object document) { Assert.notNull(document, "Document must not be null"); @@ -120,6 +122,7 @@ public ReactiveBulkOperations insert(Object document) { } @Override + @Contract("_ -> this") public ReactiveBulkOperations insert(List documents) { Assert.notNull(documents, "Documents must not be null"); @@ -130,6 +133,7 @@ public ReactiveBulkOperations insert(List documents) { } @Override + @Contract("_, _, _ -> this") public ReactiveBulkOperations updateOne(Query query, UpdateDefinition update) { Assert.notNull(query, "Query must not be null"); @@ -140,6 +144,7 @@ public ReactiveBulkOperations updateOne(Query query, UpdateDefinition update) { } @Override + @Contract("_, _ -> this") public ReactiveBulkOperations updateMulti(Query query, UpdateDefinition update) { Assert.notNull(query, "Query must not be null"); @@ -150,11 +155,13 @@ public ReactiveBulkOperations updateMulti(Query query, UpdateDefinition update) } @Override + @Contract("_, _ -> this") public ReactiveBulkOperations upsert(Query query, UpdateDefinition update) { return update(query, update, true, true); } @Override + @Contract("_ -> this") public ReactiveBulkOperations remove(Query query) { Assert.notNull(query, "Query must not be null"); @@ -169,6 +176,7 @@ public ReactiveBulkOperations remove(Query query) { } @Override + @Contract("_ -> this") public ReactiveBulkOperations remove(List removes) { Assert.notNull(removes, "Removals must not be null"); @@ -181,6 +189,7 @@ public ReactiveBulkOperations remove(List removes) { } @Override + @Contract("_, _, _ -> this") public ReactiveBulkOperations replaceOne(Query query, Object replacement, FindAndReplaceOptions options) { Assert.notNull(query, "Query must not be null"); @@ -359,7 +368,7 @@ public boolean skipEventPublishing() { return eventPublisher == null; } - @SuppressWarnings("rawtypes") + @SuppressWarnings({ "rawtypes", "NullAway" }) public Mono callback(Class callbackType, T entity, String collectionName) { if (skipEntityCallbacks()) { @@ -369,7 +378,7 @@ public Mono callback(Class callbackType, T enti return entityCallbacks.callback(callbackType, entity, collectionName); } - @SuppressWarnings("rawtypes") + @SuppressWarnings({ "rawtypes", "NullAway" }) public Mono callback(Class callbackType, T entity, Document document, String collectionName) { @@ -380,6 +389,7 @@ public Mono callback(Class callbackType, T enti return entityCallbacks.callback(callbackType, entity, document, collectionName); } + @SuppressWarnings("NullAway") public void publishEvent(ApplicationEvent event) { if (skipEventPublishing()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java index 8e78f421f4..69ade2e163 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java @@ -19,16 +19,15 @@ import reactor.core.publisher.Mono; import java.util.Collection; -import java.util.Optional; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.UncategorizedMongoDbException; import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.index.IndexDefinition; import org.springframework.data.mongodb.core.index.IndexInfo; import org.springframework.data.mongodb.core.index.ReactiveIndexOperations; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.NumberUtils; @@ -48,7 +47,7 @@ public class DefaultReactiveIndexOperations implements ReactiveIndexOperations { private final ReactiveMongoOperations mongoOperations; private final String collectionName; private final QueryMapper queryMapper; - private final Optional> type; + private final @Nullable Class type; /** * Creates a new {@link DefaultReactiveIndexOperations}. @@ -59,7 +58,7 @@ public class DefaultReactiveIndexOperations implements ReactiveIndexOperations { */ public DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, String collectionName, QueryMapper queryMapper) { - this(mongoOperations, collectionName, queryMapper, Optional.empty()); + this(mongoOperations, collectionName, queryMapper, null); } /** @@ -71,12 +70,7 @@ public DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, S * @param type used for mapping potential partial index filter expression, must not be {@literal null}. */ public DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, String collectionName, - QueryMapper queryMapper, Class type) { - this(mongoOperations, collectionName, queryMapper, Optional.of(type)); - } - - private DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, String collectionName, - QueryMapper queryMapper, Optional> type) { + QueryMapper queryMapper, @Nullable Class type) { Assert.notNull(mongoOperations, "ReactiveMongoOperations must not be null"); Assert.notNull(collectionName, "Collection must not be null"); @@ -89,13 +83,12 @@ private DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, } @Override + @SuppressWarnings("NullAway") public Mono ensureIndex(IndexDefinition indexDefinition) { return mongoOperations.execute(collectionName, collection -> { - MongoPersistentEntity entity = type - .map(val -> (MongoPersistentEntity) queryMapper.getMappingContext().getRequiredPersistentEntity(val)) - .orElseGet(() -> lookupPersistentEntity(collectionName)); + MongoPersistentEntity entity = getConfiguredEntity(); IndexOptions indexOptions = IndexConverters.indexDefinitionToIndexOptionsConverter().convert(indexDefinition); @@ -124,8 +117,7 @@ public Mono alterIndex(String name, org.springframework.data.mongodb.core. }).then(); } - @Nullable - private MongoPersistentEntity lookupPersistentEntity(String collection) { + private @Nullable MongoPersistentEntity lookupPersistentEntity(String collection) { Collection> entities = queryMapper.getMappingContext().getPersistentEntities(); @@ -152,6 +144,14 @@ public Flux getIndexInfo() { .map(IndexConverters.documentToIndexInfoConverter()::convert); } + private @Nullable MongoPersistentEntity getConfiguredEntity() { + + if (type != null) { + return queryMapper.getMappingContext().getRequiredPersistentEntity(type); + } + return lookupPersistentEntity(collectionName); + } + private IndexOptions addPartialFilterIfPresent(IndexOptions ops, Document sourceOptions, @Nullable MongoPersistentEntity entity) { @@ -164,6 +164,7 @@ private IndexOptions addPartialFilterIfPresent(IndexOptions ops, Document source queryMapper.getMappedObject((Document) sourceOptions.get(PARTIAL_FILTER_EXPRESSION_KEY), entity)); } + @SuppressWarnings("NullAway") private static IndexOptions addDefaultCollationIfRequired(IndexOptions ops, @Nullable MongoPersistentEntity entity) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java index b236b4df28..6dde79e0e8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java @@ -15,9 +15,9 @@ */ package org.springframework.data.mongodb.core; -import static java.util.UUID.*; -import static org.springframework.data.mongodb.core.query.Criteria.*; -import static org.springframework.data.mongodb.core.query.Query.*; +import static java.util.UUID.randomUUID; +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.core.query.Query.query; import java.util.ArrayList; import java.util.Arrays; @@ -28,7 +28,8 @@ import org.bson.Document; import org.bson.types.ObjectId; -import org.springframework.dao.DataAccessException; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.script.ExecutableMongoScript; import org.springframework.data.mongodb.core.script.NamedMongoScript; @@ -38,8 +39,6 @@ import org.springframework.util.StringUtils; import com.mongodb.BasicDBList; -import com.mongodb.MongoException; -import com.mongodb.client.MongoDatabase; /** * Default implementation of {@link ScriptOperations} capable of saving and executing {@link ExecutableMongoScript}. @@ -51,6 +50,7 @@ * @deprecated since 2.2. The {@code eval} command has been removed in MongoDB Server 4.2.0. */ @Deprecated +@NullUnmarked class DefaultScriptOperations implements ScriptOperations { private static final String SCRIPT_COLLECTION_NAME = "system.js"; @@ -85,38 +85,28 @@ public NamedMongoScript register(NamedMongoScript script) { } @Override - public Object execute(ExecutableMongoScript script, Object... args) { + public @Nullable Object execute(ExecutableMongoScript script, Object... args) { Assert.notNull(script, "Script must not be null"); - return mongoOperations.execute(new DbCallback() { + return mongoOperations.execute(db -> { - @Override - public Object doInDB(MongoDatabase db) throws MongoException, DataAccessException { - - Document command = new Document("$eval", script.getCode()); - BasicDBList commandArgs = new BasicDBList(); - commandArgs.addAll(Arrays.asList(convertScriptArgs(false, args))); - command.append("args", commandArgs); - return db.runCommand(command).get("retval"); - } + Document command = new Document("$eval", script.getCode()); + BasicDBList commandArgs = new BasicDBList(); + commandArgs.addAll(Arrays.asList(convertScriptArgs(false, args))); + command.append("args", commandArgs); + return db.runCommand(command).get("retval"); }); } @Override - public Object call(String scriptName, Object... args) { + public @Nullable Object call(String scriptName, Object... args) { Assert.hasText(scriptName, "ScriptName must not be null or empty"); - return mongoOperations.execute(new DbCallback() { - - @Override - public Object doInDB(MongoDatabase db) throws MongoException, DataAccessException { - - return db.runCommand(new Document("eval", String.format("%s(%s)", scriptName, convertAndJoinScriptArgs(args)))) - .get("retval"); - } - }); + return mongoOperations.execute( + db -> db.runCommand(new Document("eval", String.format("%s(%s)", scriptName, convertAndJoinScriptArgs(args)))) + .get("retval")); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultWriteConcernResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultWriteConcernResolver.java index 8b4de14e05..c445e06f8a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultWriteConcernResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultWriteConcernResolver.java @@ -15,6 +15,8 @@ */ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; + import com.mongodb.WriteConcern; /** @@ -26,7 +28,7 @@ enum DefaultWriteConcernResolver implements WriteConcernResolver { INSTANCE; - public WriteConcern resolve(MongoAction action) { + public @Nullable WriteConcern resolve(MongoAction action) { return action.getDefaultWriteConcern(); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java index f64391e8cd..601b6898b8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java @@ -19,11 +19,13 @@ * Encryption algorithms supported by MongoDB Client Side Field Level Encryption. * * @author Christoph Strobl + * @author Ross Lawley * @since 3.3 */ public final class EncryptionAlgorithms { public static final String AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"; public static final String AEAD_AES_256_CBC_HMAC_SHA_512_Random = "AEAD_AES_256_CBC_HMAC_SHA_512-Random"; + public static final String RANGE = "Range"; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityLifecycleEventDelegate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityLifecycleEventDelegate.java index 94352ad65c..ad3c2b8564 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityLifecycleEventDelegate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityLifecycleEventDelegate.java @@ -15,8 +15,8 @@ */ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.lang.Nullable; /** * Delegate class to encapsulate lifecycle event configuration and publishing. @@ -47,6 +47,7 @@ public void setEventsEnabled(boolean eventsEnabled) { * * @param event the application event. */ + @SuppressWarnings("NullAway") public void publishEvent(Object event) { if (canPublishEvent()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java index 65a5131dd1..1327656356 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java @@ -22,12 +22,15 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; import org.bson.BsonNull; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.ConversionService; import org.springframework.core.env.Environment; import org.springframework.core.env.EnvironmentCapable; +import org.springframework.core.env.StandardEnvironment; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.convert.CustomConversions; import org.springframework.data.expression.ValueEvaluationContext; @@ -39,6 +42,7 @@ import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; +import org.springframework.data.mongodb.core.CollectionOptions.EncryptedFieldsOptions; import org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper; @@ -63,7 +67,6 @@ import org.springframework.data.projection.TargetAware; import org.springframework.data.util.Optionals; import org.springframework.expression.spel.support.SimpleEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.LinkedMultiValueMap; @@ -83,6 +86,7 @@ * @author Mark Paluch * @author Christoph Strobl * @author Ben Foster + * @author Ross Lawley * @since 2.1 * @see MongoTemplate * @see ReactiveMongoTemplate @@ -375,8 +379,15 @@ public CreateCollectionOptions convertToCreateCollectionOptions(@Nullable Collec result.timeSeriesOptions(options); }); - collectionOptions.getChangeStreamOptions().ifPresent(it -> result - .changeStreamPreAndPostImagesOptions(new ChangeStreamPreAndPostImagesOptions(it.getPreAndPostImages()))); + collectionOptions.getChangeStreamOptions() // + .map(CollectionOptions.CollectionChangeStreamOptions::getPreAndPostImages) // + .map(ChangeStreamPreAndPostImagesOptions::new) // + .ifPresent(result::changeStreamPreAndPostImagesOptions); + + collectionOptions.getEncryptedFieldsOptions() // + .map(EncryptedFieldsOptions::toDocument) // + .filter(Predicate.not(Document::isEmpty)) // + .ifPresent(result::encryptedFields); return result; } @@ -413,6 +424,7 @@ interface Entity { * * @return */ + @Nullable Object getId(); /** @@ -518,10 +530,9 @@ interface AdaptibleEntity extends Entity { * Populates the identifier of the backing entity if it has an identifier property and there's no identifier * currently present. * - * @param id must not be {@literal null}. + * @param id can be {@literal null}. * @return */ - @Nullable T populateIdIfNecessary(@Nullable Object id); /** @@ -564,12 +575,12 @@ public String getIdFieldName() { } @Override - public Object getId() { + public @Nullable Object getId() { return getPropertyValue(ID_FIELD); } @Override - public Object getPropertyValue(String key) { + public @Nullable Object getPropertyValue(String key) { return map.get(key); } @@ -578,7 +589,6 @@ public Query getByIdQuery() { return Query.query(Criteria.where(ID_FIELD).is(map.get(ID_FIELD))); } - @Nullable @Override public T populateIdIfNecessary(@Nullable Object id) { @@ -605,8 +615,7 @@ public T initializeVersionProperty() { } @Override - @Nullable - public Number getVersion() { + public @Nullable Number getVersion() { return null; } @@ -723,7 +732,7 @@ public Object getId() { } @Override - public Object getPropertyValue(String key) { + public @Nullable Object getPropertyValue(String key) { return propertyAccessor.getProperty(entity.getRequiredPersistentProperty(key)); } @@ -790,8 +799,7 @@ public boolean isVersionedEntity() { } @Override - @Nullable - public Object getVersion() { + public @Nullable Object getVersion() { return propertyAccessor.getProperty(entity.getRequiredVersionProperty()); } @@ -839,7 +847,6 @@ public Map extractKeys(Document sortObject, Class sourceType) return keyset; } - @Nullable private Object getNestedPropertyValue(String key) { String[] segments = key.split("\\."); @@ -852,6 +859,10 @@ private Object getNestedPropertyValue(String key) { currentValue = currentEntity.getPropertyValue(segment); if (i < segments.length - 1) { + if (currentValue == null) { + return BsonNull.VALUE; + } + currentEntity = entityOperations.forEntity(currentValue); } } @@ -888,7 +899,6 @@ private static AdaptibleEntity of(T bean, new ConvertingPropertyAccessor<>(propertyAccessor, conversionService), entityOperations); } - @Nullable @Override public T populateIdIfNecessary(@Nullable Object id) { @@ -910,8 +920,7 @@ public T populateIdIfNecessary(@Nullable Object id) { } @Override - @Nullable - public Number getVersion() { + public @Nullable Number getVersion() { MongoPersistentProperty versionProperty = entity.getRequiredVersionProperty(); @@ -1127,7 +1136,7 @@ public TimeSeriesOptions mapTimeSeriesOptions(TimeSeriesOptions source) { @Override public String getIdKeyName() { - return entity.getIdProperty().getName(); + return entity.getIdProperty() != null ? entity.getIdProperty().getName() : ID_FIELD; } private String mappedNameOrDefault(String name) { @@ -1147,7 +1156,8 @@ private ValueEvaluationContext getEvaluationContextForEntity(@Nullable Persisten return mongoEntity.getValueEvaluationContext(null); } - return ValueEvaluationContext.of(this.environment, SimpleEvaluationContext.forReadOnlyDataBinding().build()); + return ValueEvaluationContext.of(this.environment != null ? this.environment : new StandardEnvironment(), + SimpleEvaluationContext.forReadOnlyDataBinding().build()); } /** diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/JmxServer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityResultConverter.java similarity index 52% rename from spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/JmxServer.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityResultConverter.java index 004bda1544..c04ae9d603 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/JmxServer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityResultConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2025 the original author or authors. + * 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. @@ -15,24 +15,19 @@ */ package org.springframework.data.mongodb.core; -import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.bson.Document; -/** - * Server application than can be run as an app or unit test. - * - * @author Mark Pollack - * @author Oliver Gierke - * @deprecated since 4.5. - */ -@Deprecated(since = "4.5", forRemoval = true) -public class JmxServer { +enum EntityResultConverter implements QueryResultConverter { + + INSTANCE; - public static void main(String[] args) { - new JmxServer().run(); + @Override + public Object mapDocument(Document document, ConversionResultSupplier reader) { + return reader.get(); } - @SuppressWarnings("resource") - public void run() { - new ClassPathXmlApplicationContext(new String[] { "infrastructure.xml", "server-jmx.xml" }); + @Override + public QueryResultConverter andThen(QueryResultConverter after) { + return (QueryResultConverter) after; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperation.java index 67ed188655..57813a75ba 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperation.java @@ -19,6 +19,7 @@ import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.lang.Contract; /** * {@link ExecutableAggregationOperation} allows creation and execution of MongoDB aggregation operations in a fluent @@ -45,7 +46,7 @@ public interface ExecutableAggregationOperation { /** * Start creating an aggregation operation that returns results mapped to the given domain type.
* Use {@link org.springframework.data.mongodb.core.aggregation.TypedAggregation} to specify a potentially different - * input type for he aggregation. + * input type for the aggregation. * * @param domainType must not be {@literal null}. * @return new instance of {@link ExecutableAggregation}. @@ -76,10 +77,23 @@ interface AggregationWithCollection { * Trigger execution by calling one of the terminating methods. * * @author Christoph Strobl + * @author Mark Paluch * @since 2.0 */ interface TerminatingAggregation { + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link TerminatingAggregation}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since 5.0 + */ + @Contract("_ -> new") + TerminatingAggregation map(QueryResultConverter converter); + /** * Apply pipeline operations as specified and get all matching elements. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java index ca5aa7a513..13dc8cd436 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java @@ -17,6 +17,7 @@ import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.aggregation.TypedAggregation; @@ -43,25 +44,28 @@ public ExecutableAggregation aggregateAndReturn(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); - return new ExecutableAggregationSupport<>(template, domainType, null, null); + return new ExecutableAggregationSupport<>(template, domainType, QueryResultConverter.entity(), null, null); } /** * @author Christoph Strobl * @since 2.0 */ - static class ExecutableAggregationSupport + static class ExecutableAggregationSupport implements AggregationWithAggregation, ExecutableAggregation, TerminatingAggregation { private final MongoTemplate template; - private final Class domainType; - private final Aggregation aggregation; - private final String collection; - - public ExecutableAggregationSupport(MongoTemplate template, Class domainType, Aggregation aggregation, - String collection) { + private final Class domainType; + private final QueryResultConverter resultConverter; + private final @Nullable Aggregation aggregation; + private final @Nullable String collection; + + public ExecutableAggregationSupport(MongoTemplate template, Class domainType, + QueryResultConverter resultConverter, @Nullable Aggregation aggregation, + @Nullable String collection) { this.template = template; this.domainType = domainType; + this.resultConverter = resultConverter; this.aggregation = aggregation; this.collection = collection; } @@ -71,7 +75,7 @@ public AggregationWithAggregation inCollection(String collection) { Assert.hasText(collection, "Collection must not be null nor empty"); - return new ExecutableAggregationSupport<>(template, domainType, aggregation, collection); + return new ExecutableAggregationSupport<>(template, domainType, resultConverter, aggregation, collection); } @Override @@ -79,26 +83,39 @@ public TerminatingAggregation by(Aggregation aggregation) { Assert.notNull(aggregation, "Aggregation must not be null"); - return new ExecutableAggregationSupport<>(template, domainType, aggregation, collection); + return new ExecutableAggregationSupport<>(template, domainType, resultConverter, aggregation, collection); + } + + @Override + public TerminatingAggregation map(QueryResultConverter converter) { + + Assert.notNull(converter, "QueryResultConverter must not be null"); + + return new ExecutableAggregationSupport<>(template, domainType, this.resultConverter.andThen(converter), + aggregation, collection); } @Override public AggregationResults all() { - return template.aggregate(aggregation, getCollectionName(aggregation), domainType); + + Assert.notNull(aggregation, "Aggregation must be set first"); + return template.doAggregate(aggregation, getCollectionName(aggregation), domainType, resultConverter); } @Override public Stream stream() { - return template.aggregateStream(aggregation, getCollectionName(aggregation), domainType); + + Assert.notNull(aggregation, "Aggregation must be set first"); + return template.doAggregateStream(aggregation, getCollectionName(aggregation), domainType, resultConverter, null); } - private String getCollectionName(Aggregation aggregation) { + private String getCollectionName(@Nullable Aggregation aggregation) { if (StringUtils.hasText(collection)) { return collection; } - if (aggregation instanceof TypedAggregation typedAggregation) { + if (aggregation instanceof TypedAggregation typedAggregation) { if (typedAggregation.getInputType() != null) { return template.getCollectionName(typedAggregation.getInputType()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java index 3358ff2b17..43c0d521c3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java @@ -19,6 +19,7 @@ import java.util.Optional; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessException; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.ScrollPosition; @@ -27,7 +28,7 @@ import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import com.mongodb.client.MongoCollection; @@ -71,9 +72,33 @@ public interface ExecutableFindOperation { * Trigger find execution by calling one of the terminating methods. * * @author Christoph Strobl + * @author Mark Paluch * @since 2.0 */ - interface TerminatingFind { + interface TerminatingFind extends TerminatingResults, TerminatingProjection { + + } + + /** + * Trigger find execution by calling one of the terminating methods. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 5.0 + */ + interface TerminatingResults { + + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link TerminatingResults}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since 5.0 + */ + @Contract("_ -> new") + TerminatingResults map(QueryResultConverter converter); /** * Get exactly zero or one result. @@ -132,7 +157,7 @@ default Optional first() { *

* When using {@link KeysetScrollPosition}, make sure to use non-nullable * {@link org.springframework.data.domain.Sort sort properties} as MongoDB does not support criteria to reconstruct - * a query result from absent document fields or {@code null} values through {@code $gt/$lt} operators. + * a query result from absent document fields or {@literal null} values through {@code $gt/$lt} operators. * * @param scrollPosition the scroll position. * @return a window of the resulting elements. @@ -142,6 +167,16 @@ default Optional first() { */ Window scroll(ScrollPosition scrollPosition); + } + + /** + * Trigger find execution by calling one of the terminating methods. + * + * @author Christoph Strobl + * @since 5.0 + */ + interface TerminatingProjection { + /** * Get the number of matching elements.
* This method uses an @@ -160,16 +195,30 @@ default Optional first() { * @return {@literal true} if at least one matching element exists. */ boolean exists(); + } /** - * Trigger geonear execution by calling one of the terminating methods. + * Trigger {@code geoNear} execution by calling one of the terminating methods. * * @author Christoph Strobl + * @author Mark Paluch * @since 2.0 */ interface TerminatingFindNear { + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link TerminatingFindNear}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since 5.0 + */ + @Contract("_ -> new") + TerminatingFindNear map(QueryResultConverter converter); + /** * Find all matching elements and return them as {@link org.springframework.data.geo.GeoResult}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java index 4e6c3547c5..46289ecfa4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java @@ -16,18 +16,18 @@ package org.springframework.data.mongodb.core; import java.util.List; -import java.util.Optional; import java.util.stream.Stream; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Window; +import org.springframework.data.geo.GeoResults; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.SerializationUtils; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -53,11 +53,13 @@ class ExecutableFindOperationSupport implements ExecutableFindOperation { } @Override + @Contract("_ -> new") public ExecutableFind query(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); - return new ExecutableFindSupport<>(template, domainType, domainType, null, ALL_QUERY); + return new ExecutableFindSupport<>(template, domainType, domainType, QueryResultConverter.entity(), null, + ALL_QUERY); } /** @@ -65,50 +67,66 @@ public ExecutableFind query(Class domainType) { * @author Christoph Strobl * @since 2.0 */ - static class ExecutableFindSupport + static class ExecutableFindSupport implements ExecutableFind, FindWithCollection, FindWithProjection, FindWithQuery { private final MongoTemplate template; private final Class domainType; - private final Class returnType; + private final Class returnType; + private final QueryResultConverter resultConverter; private final @Nullable String collection; private final Query query; - ExecutableFindSupport(MongoTemplate template, Class domainType, Class returnType, @Nullable String collection, + ExecutableFindSupport(MongoTemplate template, Class domainType, Class returnType, + QueryResultConverter resultConverter, @Nullable String collection, Query query) { this.template = template; this.domainType = domainType; + this.resultConverter = resultConverter; this.returnType = returnType; this.collection = collection; this.query = query; } @Override + @Contract("_ -> new") public FindWithProjection inCollection(String collection) { Assert.hasText(collection, "Collection name must not be null nor empty"); - return new ExecutableFindSupport<>(template, domainType, returnType, collection, query); + return new ExecutableFindSupport<>(template, domainType, returnType, resultConverter, collection, query); } @Override + @Contract("_ -> new") public FindWithQuery as(Class returnType) { Assert.notNull(returnType, "ReturnType must not be null"); - return new ExecutableFindSupport<>(template, domainType, returnType, collection, query); + return new ExecutableFindSupport<>(template, domainType, returnType, QueryResultConverter.entity(), collection, + query); } @Override + @Contract("_ -> new") public TerminatingFind matching(Query query) { Assert.notNull(query, "Query must not be null"); - return new ExecutableFindSupport<>(template, domainType, returnType, collection, query); + return new ExecutableFindSupport<>(template, domainType, returnType, resultConverter, collection, query); + } + + @Override + public TerminatingResults map(QueryResultConverter converter) { + + Assert.notNull(converter, "QueryResultConverter must not be null"); + + return new ExecutableFindSupport<>(template, domainType, returnType, this.resultConverter.andThen(converter), + collection, query); } @Override - public T oneValue() { + public @Nullable T oneValue() { List result = doFind(new DelegatingQueryCursorPreparer(getCursorPreparer(query, null)).limit(2)); @@ -124,7 +142,7 @@ public T oneValue() { } @Override - public T firstValue() { + public @Nullable T firstValue() { List result = doFind(new DelegatingQueryCursorPreparer(getCursorPreparer(query, null)).limit(1)); @@ -143,12 +161,13 @@ public Stream stream() { @Override public Window scroll(ScrollPosition scrollPosition) { - return template.doScroll(query.with(scrollPosition), domainType, returnType, getCollectionName()); + return template.doScroll(query.with(scrollPosition), domainType, returnType, resultConverter, + getCollectionName()); } @Override public TerminatingFindNear near(NearQuery nearQuery) { - return () -> template.geoNear(nearQuery, domainType, getCollectionName(), returnType); + return new TerminatingFindNearSupport<>(nearQuery, this.resultConverter); } @Override @@ -176,17 +195,17 @@ private List doFind(@Nullable CursorPreparer preparer) { Document fieldsObject = query.getFieldsObject(); return template.doFind(template.createDelegate(query), getCollectionName(), queryObject, fieldsObject, domainType, - returnType, getCursorPreparer(query, preparer)); + returnType, resultConverter, getCursorPreparer(query, preparer)); } private List doFindDistinct(String field) { return template.findDistinct(query, field, getCollectionName(), domainType, - returnType == domainType ? (Class) Object.class : returnType); + returnType == domainType ? (Class) Object.class : returnType); } private Stream doStream() { - return template.doStream(query, domainType, getCollectionName(), returnType); + return template.doStream(query, domainType, getCollectionName(), returnType, resultConverter); } private CursorPreparer getCursorPreparer(Query query, @Nullable CursorPreparer preparer) { @@ -200,16 +219,41 @@ private String getCollectionName() { private String asString() { return SerializationUtils.serializeToJsonSafely(query); } + + class TerminatingFindNearSupport implements TerminatingFindNear { + + private final NearQuery nearQuery; + private final QueryResultConverter resultConverter; + + public TerminatingFindNearSupport(NearQuery nearQuery, + QueryResultConverter resultConverter) { + this.nearQuery = nearQuery; + this.resultConverter = resultConverter; + } + + @Override + public TerminatingFindNear map(QueryResultConverter converter) { + + Assert.notNull(converter, "QueryResultConverter must not be null"); + + return new TerminatingFindNearSupport<>(nearQuery, this.resultConverter.andThen(converter)); + } + + @Override + public GeoResults all() { + return template.doGeoNear(nearQuery, domainType, getCollectionName(), returnType, resultConverter); + } + } } /** * @author Christoph Strobl * @since 2.0 */ - static class DelegatingQueryCursorPreparer implements SortingQueryCursorPreparer { + static class DelegatingQueryCursorPreparer implements CursorPreparer { private final @Nullable CursorPreparer delegate; - private Optional limit = Optional.empty(); + private int limit = -1; DelegatingQueryCursorPreparer(@Nullable CursorPreparer delegate) { this.delegate = delegate; @@ -219,25 +263,22 @@ static class DelegatingQueryCursorPreparer implements SortingQueryCursorPreparer public FindIterable prepare(FindIterable iterable) { FindIterable target = delegate != null ? delegate.prepare(iterable) : iterable; - return limit.map(target::limit).orElse(target); + if (limit >= 0) { + target.limit(limit); + } + return target; } + @Contract("_ -> this") CursorPreparer limit(int limit) { - this.limit = Optional.of(limit); + this.limit = limit; return this; } @Override - @Nullable - public ReadPreference getReadPreference() { - return delegate.getReadPreference(); - } - - @Override - @Nullable - public Document getSortObject() { - return delegate instanceof SortingQueryCursorPreparer sqcp ? sqcp.getSortObject() : null; + public @Nullable ReadPreference getReadPreference() { + return delegate != null ? delegate.getReadPreference() : null; } } @@ -245,19 +286,20 @@ public Document getSortObject() { * @author Christoph Strobl * @since 2.1 */ - static class DistinctOperationSupport implements TerminatingDistinct { + static class DistinctOperationSupport implements TerminatingDistinct { private final String field; - private final ExecutableFindSupport delegate; + private final ExecutableFindSupport delegate; - public DistinctOperationSupport(ExecutableFindSupport delegate, String field) { + public DistinctOperationSupport(ExecutableFindSupport delegate, String field) { this.delegate = delegate; this.field = field; } @Override - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Contract("_ -> new") public TerminatingDistinct as(Class resultType) { Assert.notNull(resultType, "ResultType must not be null"); @@ -266,16 +308,18 @@ public TerminatingDistinct as(Class resultType) { } @Override + @Contract("_ -> new") public TerminatingDistinct matching(Query query) { Assert.notNull(query, "Query must not be null"); - return new DistinctOperationSupport<>((ExecutableFindSupport) delegate.matching(query), field); + return new DistinctOperationSupport<>((ExecutableFindSupport) delegate.matching(query), field); } @Override public List all() { return delegate.doFindDistinct(field); } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupport.java index 47b7127deb..599a910035 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupport.java @@ -18,8 +18,9 @@ import java.util.ArrayList; import java.util.Collection; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.BulkOperations.BulkMode; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -41,6 +42,7 @@ class ExecutableInsertOperationSupport implements ExecutableInsertOperation { } @Override + @Contract("_ -> new") public ExecutableInsert insert(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); @@ -56,10 +58,11 @@ static class ExecutableInsertSupport implements ExecutableInsert { private final MongoTemplate template; private final Class domainType; - @Nullable private final String collection; - @Nullable private final BulkMode bulkMode; + private final @Nullable String collection; + private final @Nullable BulkMode bulkMode; - ExecutableInsertSupport(MongoTemplate template, Class domainType, String collection, BulkMode bulkMode) { + ExecutableInsertSupport(MongoTemplate template, Class domainType, @Nullable String collection, + @Nullable BulkMode bulkMode) { this.template = template; this.domainType = domainType; @@ -93,6 +96,7 @@ public BulkWriteResult bulk(Collection objects) { } @Override + @Contract("_ -> new") public InsertWithBulkMode inCollection(String collection) { Assert.hasText(collection, "Collection must not be null nor empty"); @@ -101,6 +105,7 @@ public InsertWithBulkMode inCollection(String collection) { } @Override + @Contract("_ -> new") public TerminatingBulkInsert withBulkMode(BulkMode bulkMode) { Assert.notNull(bulkMode, "BulkMode must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java index 9f78693540..55864cbd8e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java @@ -17,9 +17,10 @@ import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -46,6 +47,7 @@ class ExecutableMapReduceOperationSupport implements ExecutableMapReduceOperatio * @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation#mapReduce(java.lang.Class) */ @Override + @Contract("_ -> new") public ExecutableMapReduceSupport mapReduce(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); @@ -89,6 +91,7 @@ static class ExecutableMapReduceSupport * @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.TerminatingMapReduce#all() */ @Override + @SuppressWarnings("NullAway") public List all() { return template.mapReduce(query, domainType, getCollectionName(), mapFunction, reduceFunction, options, returnType); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java index a10cd0317f..c29a448f1c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java @@ -19,6 +19,7 @@ import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.lang.Contract; import com.mongodb.client.result.DeleteResult; @@ -54,11 +55,40 @@ public interface ExecutableRemoveOperation { */ ExecutableRemove remove(Class domainType); + /** + * @author Christoph Strobl + * @since 5.0 + */ + interface TerminatingResults { + + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link ExecutableFindOperation.TerminatingResults}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since 5.0 + */ + @Contract("_ -> new") + TerminatingResults map(QueryResultConverter converter); + + /** + * Remove and return all matching documents.
+ * NOTE: The entire list of documents will be fetched before sending the actual delete commands. + * Also, {@link org.springframework.context.ApplicationEvent}s will be published for each and every delete + * operation. + * + * @return empty {@link List} if no match found. Never {@literal null}. + */ + List findAndRemove(); + } + /** * @author Christoph Strobl * @since 2.0 */ - interface TerminatingRemove { + interface TerminatingRemove extends TerminatingResults { /** * Remove all documents matching. @@ -73,16 +103,6 @@ interface TerminatingRemove { * @return the {@link DeleteResult}. Never {@literal null}. */ DeleteResult one(); - - /** - * Remove and return all matching documents.
- * NOTE: The entire list of documents will be fetched before sending the actual delete commands. - * Also, {@link org.springframework.context.ApplicationEvent}s will be published for each and every delete - * operation. - * - * @return empty {@link List} if no match found. Never {@literal null}. - */ - List findAndRemove(); } /** @@ -105,7 +125,6 @@ interface RemoveWithCollection extends RemoveWithQuery { RemoveWithQuery inCollection(String collection); } - /** * @author Christoph Strobl * @since 2.0 diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java index 8e84aa7dd6..7817a7c8af 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java @@ -17,8 +17,9 @@ import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -42,45 +43,51 @@ public ExecutableRemoveOperationSupport(MongoTemplate tempate) { } @Override + @Contract("_ -> new") public ExecutableRemove remove(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); - return new ExecutableRemoveSupport<>(tempate, domainType, ALL_QUERY, null); + return new ExecutableRemoveSupport<>(tempate, domainType, ALL_QUERY, null, QueryResultConverter.entity()); } /** * @author Christoph Strobl * @since 2.0 */ - static class ExecutableRemoveSupport implements ExecutableRemove, RemoveWithCollection { + static class ExecutableRemoveSupport implements ExecutableRemove, RemoveWithCollection { private final MongoTemplate template; - private final Class domainType; + private final Class domainType; private final Query query; @Nullable private final String collection; + private final QueryResultConverter resultConverter; - public ExecutableRemoveSupport(MongoTemplate template, Class domainType, Query query, String collection) { + public ExecutableRemoveSupport(MongoTemplate template, Class domainType, Query query, + @Nullable String collection, QueryResultConverter resultConverter) { this.template = template; this.domainType = domainType; this.query = query; this.collection = collection; + this.resultConverter = resultConverter; } @Override + @Contract("_ -> new") public RemoveWithQuery inCollection(String collection) { Assert.hasText(collection, "Collection must not be null nor empty"); - return new ExecutableRemoveSupport<>(template, domainType, query, collection); + return new ExecutableRemoveSupport<>(template, domainType, query, collection, resultConverter); } @Override + @Contract("_ -> new") public TerminatingRemove matching(Query query) { Assert.notNull(query, "Query must not be null"); - return new ExecutableRemoveSupport<>(template, domainType, query, collection); + return new ExecutableRemoveSupport<>(template, domainType, query, collection, resultConverter); } @Override @@ -98,7 +105,13 @@ public List findAndRemove() { String collectionName = getCollectionName(); - return template.doFindAndDelete(collectionName, query, domainType); + return template.doFindAndDelete(collectionName, query, domainType, resultConverter); + } + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public TerminatingResults map(QueryResultConverter converter) { + return new ExecutableRemoveSupport<>(template, (Class) domainType, query, collection, converter); } private String getCollectionName() { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java index a5c63e9b67..e671b7b7ce 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java @@ -17,12 +17,14 @@ import java.util.Optional; +import org.jspecify.annotations.Nullable; + import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import com.mongodb.client.result.UpdateResult; @@ -69,6 +71,18 @@ public interface ExecutableUpdateOperation { */ interface TerminatingFindAndModify { + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link TerminatingFindAndModify}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since 5.0 + */ + @Contract("_ -> new") + TerminatingFindAndModify map(QueryResultConverter converter); + /** * Find, modify and return the first matching document. * @@ -130,6 +144,19 @@ default Optional findAndReplace() { */ @Nullable T findAndReplaceValue(); + + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link TerminatingFindAndModify}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since 5.0 + */ + @Contract("_ -> new") + TerminatingFindAndReplace map(QueryResultConverter converter); + } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java index 593d863d39..dc9ce5cacc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java @@ -15,9 +15,10 @@ */ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -41,34 +42,38 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { } @Override + @Contract("_ -> new") public ExecutableUpdate update(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); - return new ExecutableUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null, domainType); + return new ExecutableUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null, domainType, QueryResultConverter.entity()); } /** * @author Christoph Strobl * @since 2.0 */ - static class ExecutableUpdateSupport + @SuppressWarnings("rawtypes") + static class ExecutableUpdateSupport implements ExecutableUpdate, UpdateWithCollection, UpdateWithQuery, TerminatingUpdate, FindAndReplaceWithOptions, TerminatingFindAndReplace, FindAndReplaceWithProjection { private final MongoTemplate template; - private final Class domainType; + private final Class domainType; private final Query query; @Nullable private final UpdateDefinition update; @Nullable private final String collection; @Nullable private final FindAndModifyOptions findAndModifyOptions; @Nullable private final FindAndReplaceOptions findAndReplaceOptions; @Nullable private final Object replacement; - private final Class targetType; + private final QueryResultConverter resultConverter; + private final Class targetType; - ExecutableUpdateSupport(MongoTemplate template, Class domainType, Query query, UpdateDefinition update, - String collection, FindAndModifyOptions findAndModifyOptions, FindAndReplaceOptions findAndReplaceOptions, - Object replacement, Class targetType) { + ExecutableUpdateSupport(MongoTemplate template, Class domainType, Query query, @Nullable UpdateDefinition update, + @Nullable String collection, @Nullable FindAndModifyOptions findAndModifyOptions, + @Nullable FindAndReplaceOptions findAndReplaceOptions, @Nullable Object replacement, Class targetType, + QueryResultConverter resultConverter) { this.template = template; this.domainType = domainType; @@ -79,54 +84,61 @@ static class ExecutableUpdateSupport this.findAndReplaceOptions = findAndReplaceOptions; this.replacement = replacement; this.targetType = targetType; + this.resultConverter = resultConverter; } @Override + @Contract("_ -> new") public TerminatingUpdate apply(UpdateDefinition update) { Assert.notNull(update, "Update must not be null"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override + @Contract("_ -> new") public UpdateWithQuery inCollection(String collection) { Assert.hasText(collection, "Collection must not be null nor empty"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override + @Contract("_ -> new") public TerminatingFindAndModify withOptions(FindAndModifyOptions options) { Assert.notNull(options, "Options must not be null"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, options, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override + @Contract("_ -> new") public FindAndReplaceWithProjection replaceWith(T replacement) { Assert.notNull(replacement, "Replacement must not be null"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override + @Contract("_ -> new") public FindAndReplaceWithProjection withOptions(FindAndReplaceOptions options) { Assert.notNull(options, "Options must not be null"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - options, replacement, targetType); + options, replacement, targetType, resultConverter); } @Override + @Contract("_ -> new") public TerminatingReplace withOptions(ReplaceOptions options) { FindAndReplaceOptions target = new FindAndReplaceOptions(); @@ -134,25 +146,27 @@ public TerminatingReplace withOptions(ReplaceOptions options) { target.upsert(); } return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - target, replacement, targetType); + target, replacement, targetType, resultConverter); } @Override + @Contract("_ -> new") public UpdateWithUpdate matching(Query query) { Assert.notNull(query, "Query must not be null"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override + @Contract("_ -> new") public FindAndReplaceWithOptions as(Class resultType) { Assert.notNull(resultType, "ResultType must not be null"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, resultType); + findAndReplaceOptions, replacement, resultType, QueryResultConverter.entity()); } @Override @@ -171,22 +185,31 @@ public UpdateResult upsert() { } @Override + public ExecutableUpdateSupport map(QueryResultConverter converter) { + return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, + findAndReplaceOptions, replacement, targetType, this.resultConverter.andThen(converter)); + } + + @Override + @SuppressWarnings("NullAway") public @Nullable T findAndModifyValue() { return template.findAndModify(query, update, findAndModifyOptions != null ? findAndModifyOptions : new FindAndModifyOptions(), targetType, - getCollectionName()); + getCollectionName(), resultConverter); } @Override + @SuppressWarnings({ "unchecked", "NullAway" }) public @Nullable T findAndReplaceValue() { return (T) template.findAndReplace(query, replacement, - findAndReplaceOptions != null ? findAndReplaceOptions : FindAndReplaceOptions.empty(), domainType, - getCollectionName(), targetType); + findAndReplaceOptions != null ? findAndReplaceOptions : FindAndReplaceOptions.empty(), (Class) domainType, + getCollectionName(), targetType, (QueryResultConverter) resultConverter); } @Override + @SuppressWarnings({ "unchecked", "NullAway" }) public UpdateResult replaceFirst() { if (replacement != null) { @@ -198,6 +221,7 @@ public UpdateResult replaceFirst() { findAndReplaceOptions != null ? findAndReplaceOptions : ReplaceOptions.none(), getCollectionName()); } + @SuppressWarnings("NullAway") private UpdateResult doUpdate(boolean multi, boolean upsert) { return template.doUpdate(getCollectionName(), query, update, domainType, upsert, multi); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java index 51a2c5b86a..6e9b775324 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java @@ -17,8 +17,9 @@ import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Collation; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; /** * @author Mark Pollak @@ -99,16 +100,19 @@ public static FindAndModifyOptions of(@Nullable FindAndModifyOptions source) { return options; } + @Contract("_ -> this") public FindAndModifyOptions returnNew(boolean returnNew) { this.returnNew = returnNew; return this; } + @Contract("_ -> this") public FindAndModifyOptions upsert(boolean upsert) { this.upsert = upsert; return this; } + @Contract("_ -> this") public FindAndModifyOptions remove(boolean remove) { this.remove = remove; return this; @@ -121,6 +125,7 @@ public FindAndModifyOptions remove(boolean remove) { * @return this. * @since 2.0 */ + @Contract("_ -> this") public FindAndModifyOptions collation(@Nullable Collation collation) { this.collation = collation; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java index 266a0742c2..2005ba3c6c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java @@ -15,6 +15,8 @@ */ package org.springframework.data.mongodb.core; +import org.springframework.lang.Contract; + /** * Options for * findOneAndReplace. @@ -95,6 +97,7 @@ public static FindAndReplaceOptions empty() { * * @return this. */ + @Contract("-> this") public FindAndReplaceOptions returnNew() { this.returnNew = true; @@ -106,6 +109,7 @@ public FindAndReplaceOptions returnNew() { * * @return this. */ + @Contract("-> this") public FindAndReplaceOptions upsert() { super.upsert(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindPublisherPreparer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindPublisherPreparer.java index 625a85950e..f04417325c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindPublisherPreparer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindPublisherPreparer.java @@ -18,7 +18,7 @@ import java.util.function.Function; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import com.mongodb.ReadPreference; @@ -76,8 +76,7 @@ default FindPublisher initiateFind(MongoCollection collectio * @since 2.2 */ @Override - @Nullable - default ReadPreference getReadPreference() { + default @Nullable ReadPreference getReadPreference() { return null; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/HintFunction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/HintFunction.java index 57abe9a529..043613122a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/HintFunction.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/HintFunction.java @@ -18,9 +18,9 @@ import java.util.function.Function; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.CodecRegistryProvider; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java index f5856100d0..9f9295bba3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java @@ -18,12 +18,10 @@ import java.util.concurrent.TimeUnit; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; import org.springframework.data.mongodb.core.index.IndexDefinition; import org.springframework.data.mongodb.core.index.IndexInfo; -import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import com.mongodb.client.model.Collation; @@ -90,9 +88,6 @@ private static Converter getIndexDefinitionIndexO if (indexOptions.containsKey("bits")) { ops = ops.bits((Integer) indexOptions.get("bits")); } - if (indexOptions.containsKey("bucketSize")) { - MongoCompatibilityAdapter.indexOptionsAdapter(ops).setBucketSize(((Number) indexOptions.get("bucketSize")).doubleValue()); - } if (indexOptions.containsKey("default_language")) { ops = ops.defaultLanguage(indexOptions.get("default_language").toString()); } @@ -129,8 +124,7 @@ private static Converter getIndexDefinitionIndexO }; } - @Nullable - public static Collation fromDocument(@Nullable Document source) { + public static @Nullable Collation fromDocument(@Nullable Document source) { if (source == null) { return null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java index da4766343a..cd9ba90453 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java @@ -20,6 +20,7 @@ import org.bson.Document; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; @@ -70,7 +71,7 @@ public boolean hasNonNullId() { return hasId() && document.get(ID_FIELD) != null; } - public Object getId() { + public @Nullable Object getId() { return document.get(ID_FIELD); } @@ -86,7 +87,7 @@ public Bson getIdFilter() { return new Document(ID_FIELD, document.get(ID_FIELD)); } - public Object get(String key) { + public @Nullable Object get(String key) { return document.get(key); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java index 839f49c7da..396ae1ce8a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java @@ -24,6 +24,7 @@ import java.util.stream.Collectors; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.convert.MongoConverter; @@ -31,16 +32,21 @@ import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.core.mapping.Queryable; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ArrayJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ObjectJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.QueryableJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.JsonSchemaObject; import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type; import org.springframework.data.mongodb.core.schema.JsonSchemaProperty; import org.springframework.data.mongodb.core.schema.MongoJsonSchema; import org.springframework.data.mongodb.core.schema.MongoJsonSchema.MongoJsonSchemaBuilder; +import org.springframework.data.mongodb.core.schema.QueryCharacteristic; +import org.springframework.data.mongodb.core.schema.QueryCharacteristics; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject; import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -89,6 +95,7 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator { } @Override + @Contract("_ -> new") public MongoJsonSchemaCreator filter(Predicate filter) { return new MappingMongoJsonSchemaCreator(converter, mappingContext, filter, mergeProperties); } @@ -106,6 +113,7 @@ public PropertySpecifier property(String path) { * @return new instance of {@link MongoJsonSchemaCreator}. * @since 3.4 */ + @Contract("_, _ -> new") public MongoJsonSchemaCreator withTypesFor(String path, Class... types) { LinkedMultiValueMap> clone = mergeProperties.clone(); @@ -121,29 +129,31 @@ public MongoJsonSchema createSchemaFor(Class type) { MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(type); MongoJsonSchemaBuilder schemaBuilder = MongoJsonSchema.builder(); - { - Encrypted encrypted = entity.findAnnotation(Encrypted.class); - if (encrypted != null) { + Encrypted encrypted = entity.findAnnotation(Encrypted.class); + if (encrypted != null) { + schemaBuilder.encryptionMetadata(getEncryptionMetadata(entity, encrypted)); + } - Document encryptionMetadata = new Document(); + List schemaProperties = computePropertiesForEntity(Collections.emptyList(), entity); + schemaBuilder.properties(schemaProperties.toArray(new JsonSchemaProperty[0])); - Collection encryptionKeyIds = entity.getEncryptionKeyIds(); - if (!CollectionUtils.isEmpty(encryptionKeyIds)) { - encryptionMetadata.append("keyId", encryptionKeyIds); - } + return schemaBuilder.build(); + } - if (StringUtils.hasText(encrypted.algorithm())) { - encryptionMetadata.append("algorithm", encrypted.algorithm()); - } + private static Document getEncryptionMetadata(MongoPersistentEntity entity, Encrypted encrypted) { - schemaBuilder.encryptionMetadata(encryptionMetadata); - } + Document encryptionMetadata = new Document(); + + Collection encryptionKeyIds = entity.getEncryptionKeyIds(); + if (!CollectionUtils.isEmpty(encryptionKeyIds)) { + encryptionMetadata.append("keyId", encryptionKeyIds); } - List schemaProperties = computePropertiesForEntity(Collections.emptyList(), entity); - schemaBuilder.properties(schemaProperties.toArray(new JsonSchemaProperty[0])); + if (StringUtils.hasText(encrypted.algorithm())) { + encryptionMetadata.append("algorithm", encrypted.algorithm()); + } - return schemaBuilder.build(); + return encryptionMetadata; } private List computePropertiesForEntity(List path, @@ -176,6 +186,7 @@ private List computePropertiesForEntity(List path) { String stringPath = path.stream().map(MongoPersistentProperty::getName).collect(Collectors.joining(".")); @@ -185,8 +196,8 @@ private JsonSchemaProperty computeSchemaForProperty(List rawTargetType = computeTargetType(property); // target type before conversion Class targetType = converter.getTypeMapper().getWriteTargetTypeFor(rawTargetType); // conversion target type - - if ((rawTargetType.isPrimitive() || ClassUtils.isPrimitiveArray(rawTargetType)) && targetType == Object.class || ClassUtils.isAssignable(targetType, rawTargetType) ) { + if ((rawTargetType.isPrimitive() || ClassUtils.isPrimitiveArray(rawTargetType)) && targetType == Object.class + || ClassUtils.isAssignable(targetType, rawTargetType)) { targetType = rawTargetType; } @@ -291,7 +302,36 @@ private JsonSchemaProperty applyEncryptionDataIfNecessary(MongoPersistentPropert if (!ObjectUtils.isEmpty(encrypted.keyId())) { enc = enc.keys(property.getEncryptionKeyIds()); } - return enc; + + Queryable queryable = property.findAnnotation(Queryable.class); + if (queryable == null || !StringUtils.hasText(queryable.queryType())) { + return enc; + } + + QueryCharacteristic characteristic = new QueryCharacteristic() { + + @Override + public String queryType() { + return queryable.queryType(); + } + + @Override + public Document toDocument() { + + Document options = QueryCharacteristic.super.toDocument(); + + if (queryable.contentionFactor() >= 0) { + options.put("contention", queryable.contentionFactor()); + } + + if (StringUtils.hasText(queryable.queryAttributes())) { + options.putAll(Document.parse(queryable.queryAttributes())); + } + + return options; + } + }; + return new QueryableJsonSchemaProperty(enc, QueryCharacteristics.of(characteristic)); } private JsonSchemaProperty createObjectSchemaPropertyForEntity(List path, @@ -337,7 +377,9 @@ private TypedJsonSchemaObject createSchemaObject(Object type, Collection poss return schemaObject; } - private String computePropertyFieldName(PersistentProperty property) { + private String computePropertyFieldName(@Nullable PersistentProperty property) { + + Assert.notNull(property, "Property must not be null"); return property instanceof MongoPersistentProperty mongoPersistentProperty ? mongoPersistentProperty.getFieldName() : property.getName(); @@ -409,7 +451,8 @@ public MongoPersistentProperty getProperty() { } @Override - public MongoPersistentEntity resolveEntity(MongoPersistentProperty property) { + @SuppressWarnings("unchecked") + public @Nullable MongoPersistentEntity resolveEntity(MongoPersistentProperty property) { return (MongoPersistentEntity) mappingContext.getPersistentEntity(property); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoAction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoAction.java index fdfeaa81ad..c827c5b8a9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoAction.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoAction.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import com.mongodb.WriteConcern; @@ -72,28 +72,23 @@ public String getCollectionName() { return collectionName; } - @Nullable - public WriteConcern getDefaultWriteConcern() { + public @Nullable WriteConcern getDefaultWriteConcern() { return defaultWriteConcern; } - @Nullable - public Class getEntityType() { + public @Nullable Class getEntityType() { return entityType; } - @Nullable - public MongoActionOperation getMongoActionOperation() { + public @Nullable MongoActionOperation getMongoActionOperation() { return mongoActionOperation; } - @Nullable - public Document getQuery() { + public @Nullable Document getQuery() { return query; } - @Nullable - public Document getDocument() { + public @Nullable Document getDocument() { return document; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java index c5fee9cf54..9210dd85ec 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java @@ -24,11 +24,11 @@ import java.util.stream.Collectors; import org.bson.UuidRepresentation; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.config.AbstractFactoryBean; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.mongodb.SpringDataMongoDB; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -78,7 +78,7 @@ public void setMongoClientSettings(@Nullable MongoClientSettings mongoClientOpti * * @param credential can be {@literal null}. */ - public void setCredential(@Nullable MongoCredential[] credential) { + public void setCredential(MongoCredential @Nullable[] credential) { this.credential = Arrays.asList(credential); } @@ -119,8 +119,7 @@ public void setExceptionTranslator(@Nullable PersistenceExceptionTranslator exce } @Override - @Nullable - public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + public @Nullable DataAccessException translateExceptionIfPossible(RuntimeException ex) { return exceptionTranslator.translateExceptionIfPossible(ex); } @@ -316,13 +315,13 @@ private void applySettings(Consumer settingsBuilder, @Nullable T value) { settingsBuilder.accept(value); } - private T computeSettingsValue(Function function, S defaultValueHolder, S settingsValueHolder, + private @Nullable T computeSettingsValue(Function function, S defaultValueHolder, S settingsValueHolder, @Nullable T connectionStringValue) { return computeSettingsValue(function.apply(defaultValueHolder), function.apply(settingsValueHolder), connectionStringValue); } - private T computeSettingsValue(T defaultValue, T fromSettings, T fromConnectionString) { + private @Nullable T computeSettingsValue(@Nullable T defaultValue, T fromSettings, @Nullable T fromConnectionString) { boolean fromSettingsIsDefault = ObjectUtils.nullSafeEquals(defaultValue, fromSettings); boolean fromConnectionStringIsDefault = ObjectUtils.nullSafeEquals(defaultValue, fromConnectionString); @@ -337,7 +336,7 @@ private MongoClient createMongoClient(MongoClientSettings settings) throws Unkno return MongoClients.create(settings, SpringDataMongoDB.driverInformation()); } - private String getOrDefault(Object value, String defaultValue) { + private String getOrDefault(@Nullable Object value, String defaultValue) { if(value == null) { return defaultValue; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java index 02913b4303..813d3a4a04 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java @@ -25,10 +25,8 @@ import org.bson.UuidRepresentation; import org.bson.codecs.configuration.CodecRegistry; - +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.config.AbstractFactoryBean; -import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -57,8 +55,6 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean new") public MongoDatabaseFactory withSession(ClientSession session) { return new MongoDatabaseFactorySupport.ClientSessionBoundMongoDbFactory(session, this); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoEncryptionSettingsFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoEncryptionSettingsFactoryBean.java index 7aef5a3a82..f361b19bba 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoEncryptionSettingsFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoEncryptionSettingsFactoryBean.java @@ -19,8 +19,8 @@ import java.util.Map; import org.bson.BsonDocument; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.FactoryBean; -import org.springframework.lang.Nullable; import com.mongodb.AutoEncryptionSettings; import com.mongodb.MongoClientSettings; @@ -34,11 +34,11 @@ public class MongoEncryptionSettingsFactoryBean implements FactoryBean { private boolean bypassAutoEncryption; - private String keyVaultNamespace; - private Map extraOptions; - private MongoClientSettings keyVaultClientSettings; - private Map> kmsProviders; - private Map schemaMap; + private @Nullable String keyVaultNamespace; + private @Nullable Map extraOptions; + private @Nullable MongoClientSettings keyVaultClientSettings; + private @Nullable Map> kmsProviders; + private @Nullable Map schemaMap; /** * @param bypassAutoEncryption diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java index 1ec7d3ffc0..2bde873c2f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java @@ -18,7 +18,7 @@ import java.util.Set; import org.bson.BsonInvalidOperationException; - +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.dao.DataIntegrityViolationException; @@ -31,7 +31,6 @@ import org.springframework.data.mongodb.TransientClientSessionException; import org.springframework.data.mongodb.UncategorizedMongoDbException; import org.springframework.data.mongodb.util.MongoDbErrorCodes; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import com.mongodb.MongoBulkWriteException; @@ -69,12 +68,12 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator private static final Set SECURITY_EXCEPTIONS = Set.of("MongoCryptException"); @Override - @Nullable - public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + public @Nullable DataAccessException translateExceptionIfPossible(RuntimeException ex) { return doTranslateException(ex); } @Nullable + @SuppressWarnings("NullAway") DataAccessException doTranslateException(RuntimeException ex) { // Check for well-known MongoException subclasses. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoJsonSchemaCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoJsonSchemaCreator.java index 66b1cf209e..84c395bf2f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoJsonSchemaCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoJsonSchemaCreator.java @@ -139,8 +139,7 @@ interface JsonSchemaPropertyContext { * @return {@literal null} if the property is not an entity. It is nevertheless recommend to check * {@link PersistentProperty#isEntity()} first. */ - @Nullable - MongoPersistentEntity resolveEntity(MongoPersistentProperty property); + @Nullable MongoPersistentEntity resolveEntity(MongoPersistentProperty property); } @@ -162,6 +161,7 @@ public boolean test(JsonSchemaPropertyContext context) { return extracted(context.getProperty(), context); } + @SuppressWarnings("NullAway") private boolean extracted(MongoPersistentProperty property, JsonSchemaPropertyContext context) { if (property.isAnnotationPresent(Encrypted.class)) { return true; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java index 65396bc7fe..6753f31c1a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java @@ -24,6 +24,7 @@ import java.util.stream.Stream; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Window; @@ -49,7 +50,6 @@ import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.util.Lock; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -196,7 +196,8 @@ default SessionScoped withSession(Supplier sessionProvider) { private @Nullable ClientSession session; @Override - public T execute(SessionCallback action, Consumer onComplete) { + @SuppressWarnings("NullAway") + public @Nullable T execute(SessionCallback action, Consumer onComplete) { lock.executeWithoutResult(() -> { @@ -733,8 +734,7 @@ MapReduceResults mapReduce(Query query, String inputCollectionName, Strin * @param entityClass the parametrized type of the returned list. * @return the converted object. */ - @Nullable - T findOne(Query query, Class entityClass); + @Nullable T findOne(Query query, Class entityClass); /** * Map the results of an ad-hoc query on the specified collection to a single instance of an object of the specified @@ -750,8 +750,7 @@ MapReduceResults mapReduce(Query query, String inputCollectionName, Strin * @param collectionName name of the collection to retrieve the objects from. * @return the converted object. */ - @Nullable - T findOne(Query query, Class entityClass, String collectionName); + @Nullable T findOne(Query query, Class entityClass, String collectionName); /** * Determine result of given {@link Query} contains at least one element.
@@ -823,7 +822,7 @@ MapReduceResults mapReduce(Query query, String inputCollectionName, Strin *

* When using {@link KeysetScrollPosition}, make sure to use non-nullable {@link org.springframework.data.domain.Sort * sort properties} as MongoDB does not support criteria to reconstruct a query result from absent document fields or - * {@code null} values through {@code $gt/$lt} operators. + * {@literal null} values through {@code $gt/$lt} operators. * * @param query the query class that specifies the criteria used to find a document and also an optional fields * specification. Must not be {@literal null}. @@ -848,7 +847,7 @@ MapReduceResults mapReduce(Query query, String inputCollectionName, Strin *

* When using {@link KeysetScrollPosition}, make sure to use non-nullable {@link org.springframework.data.domain.Sort * sort properties} as MongoDB does not support criteria to reconstruct a query result from absent document fields or - * {@code null} values through {@code $gt/$lt} operators. + * {@literal null} values through {@code $gt/$lt} operators. * * @param query the query class that specifies the criteria used to find a document and also an optional fields * specification. Must not be {@literal null}. @@ -871,8 +870,7 @@ MapReduceResults mapReduce(Query query, String inputCollectionName, Strin * @param entityClass the type the document shall be converted into. Must not be {@literal null}. * @return the document with the given id mapped onto the given target class. */ - @Nullable - T findById(Object id, Class entityClass); + @Nullable T findById(Object id, Class entityClass); /** * Returns the document with the given id from the given collection mapped onto the given target class. @@ -882,8 +880,7 @@ MapReduceResults mapReduce(Query query, String inputCollectionName, Strin * @param collectionName the collection to query for the document. * @return he converted object or {@literal null} if document does not exist. */ - @Nullable - T findById(Object id, Class entityClass, String collectionName); + @Nullable T findById(Object id, Class entityClass, String collectionName); /** * Finds the distinct values for a specified {@literal field} across a single {@link MongoCollection} or view and @@ -960,8 +957,7 @@ default List findDistinct(Query query, String field, String collection, C * @see Update * @see AggregationUpdate */ - @Nullable - T findAndModify(Query query, UpdateDefinition update, Class entityClass); + @Nullable T findAndModify(Query query, UpdateDefinition update, Class entityClass); /** * Triggers findAndModify @@ -980,8 +976,7 @@ default List findDistinct(Query query, String field, String collection, C * @see Update * @see AggregationUpdate */ - @Nullable - T findAndModify(Query query, UpdateDefinition update, Class entityClass, String collectionName); + @Nullable T findAndModify(Query query, UpdateDefinition update, Class entityClass, String collectionName); /** * Triggers findAndModify @@ -1003,8 +998,7 @@ default List findDistinct(Query query, String field, String collection, C * @see Update * @see AggregationUpdate */ - @Nullable - T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass); + @Nullable T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass); /** * Triggers findAndModify @@ -1027,8 +1021,7 @@ default List findDistinct(Query query, String field, String collection, C * @see Update * @see AggregationUpdate */ - @Nullable - T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass, + @Nullable T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass, String collectionName); /** @@ -1048,8 +1041,7 @@ T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions o * {@link #getCollectionName(Class) derived} from the given replacement value. * @since 2.1 */ - @Nullable - default T findAndReplace(Query query, T replacement) { + default @Nullable T findAndReplace(Query query, T replacement) { return findAndReplace(query, replacement, FindAndReplaceOptions.empty()); } @@ -1068,8 +1060,7 @@ default T findAndReplace(Query query, T replacement) { * @return the converted object that was updated or {@literal null}, if not found. * @since 2.1 */ - @Nullable - default T findAndReplace(Query query, T replacement, String collectionName) { + default @Nullable T findAndReplace(Query query, T replacement, String collectionName) { return findAndReplace(query, replacement, FindAndReplaceOptions.empty(), collectionName); } @@ -1091,8 +1082,7 @@ default T findAndReplace(Query query, T replacement, String collectionName) * {@link #getCollectionName(Class) derived} from the given replacement value. * @since 2.1 */ - @Nullable - default T findAndReplace(Query query, T replacement, FindAndReplaceOptions options) { + default @Nullable T findAndReplace(Query query, T replacement, FindAndReplaceOptions options) { return findAndReplace(query, replacement, options, getCollectionName(ClassUtils.getUserClass(replacement))); } @@ -1112,8 +1102,7 @@ default T findAndReplace(Query query, T replacement, FindAndReplaceOptions o * as it is after the update. * @since 2.1 */ - @Nullable - default T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName) { + default @Nullable T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName) { Assert.notNull(replacement, "Replacement must not be null"); return findAndReplace(query, replacement, options, (Class) ClassUtils.getUserClass(replacement), collectionName); @@ -1137,8 +1126,7 @@ default T findAndReplace(Query query, T replacement, FindAndReplaceOptions o * as it is after the update. * @since 2.1 */ - @Nullable - default T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class entityType, + default @Nullable T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class entityType, String collectionName) { return findAndReplace(query, replacement, options, entityType, collectionName, entityType); @@ -1166,8 +1154,7 @@ default T findAndReplace(Query query, T replacement, FindAndReplaceOptions o * {@link #getCollectionName(Class) derived} from the given replacement value. * @since 2.1 */ - @Nullable - default T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class entityType, + default @Nullable T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class entityType, Class resultType) { return findAndReplace(query, replacement, options, entityType, @@ -1194,8 +1181,7 @@ default T findAndReplace(Query query, S replacement, FindAndReplaceOption * as it is after the update. * @since 2.1 */ - @Nullable - T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class entityType, + @Nullable T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class entityType, String collectionName, Class resultType); /** @@ -1211,8 +1197,7 @@ T findAndReplace(Query query, S replacement, FindAndReplaceOptions option * @param entityClass the parametrized type of the returned list. * @return the converted object */ - @Nullable - T findAndRemove(Query query, Class entityClass); + @Nullable T findAndRemove(Query query, Class entityClass); /** * Map the results of an ad-hoc query on the specified collection to a single instance of an object of the specified @@ -1229,8 +1214,7 @@ T findAndReplace(Query query, S replacement, FindAndReplaceOptions option * @param collectionName name of the collection to retrieve the objects from. * @return the converted object. */ - @Nullable - T findAndRemove(Query query, Class entityClass, String collectionName); + @Nullable T findAndRemove(Query query, Class entityClass, String collectionName); /** * Returns the number of documents for the given {@link Query} by querying the collection of the given entity class. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoServerApiFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoServerApiFactoryBean.java index 37001faa4e..574c0c8931 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoServerApiFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoServerApiFactoryBean.java @@ -15,8 +15,8 @@ */ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.FactoryBean; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import com.mongodb.ServerApi; @@ -31,7 +31,7 @@ */ public class MongoServerApiFactoryBean implements FactoryBean { - private String version; + private @Nullable String version; private @Nullable Boolean deprecationErrors; private @Nullable Boolean strict; @@ -59,9 +59,8 @@ public void setStrict(@Nullable Boolean strict) { this.strict = strict; } - @Nullable @Override - public ServerApi getObject() throws Exception { + public @Nullable ServerApi getObject() throws Exception { Builder builder = ServerApi.builder().version(version()); @@ -81,6 +80,11 @@ public Class getObjectType() { } private ServerApiVersion version() { + + if(version == null) { + return ServerApiVersion.V1; + } + try { // lookup by name eg. 'V1' return ObjectUtils.caseInsensitiveValueOf(ServerApiVersion.values(), version); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 67ef3a3081..ab03b41424 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -30,6 +30,7 @@ import org.apache.commons.logging.LogFactory; import org.bson.Document; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; @@ -100,6 +101,7 @@ import org.springframework.data.mongodb.core.mapreduce.MapReduceResults; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Meta; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; @@ -107,11 +109,11 @@ import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter; import org.springframework.data.mongodb.core.timeseries.Granularity; import org.springframework.data.mongodb.core.validation.Validator; -import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; import org.springframework.data.projection.EntityProjection; import org.springframework.data.util.CloseableIterator; +import org.springframework.data.util.Lazy; import org.springframework.data.util.Optionals; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -343,7 +345,7 @@ public boolean hasReadPreference() { } @Override - public ReadPreference getReadPreference() { + public @Nullable ReadPreference getReadPreference() { return this.readPreference; } @@ -479,15 +481,21 @@ public Stream stream(Query query, Class entityType, String collectionN return doStream(query, entityType, collectionName, entityType); } - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) protected Stream doStream(Query query, Class entityType, String collectionName, Class returnType) { + return doStream(query, entityType, collectionName, returnType, QueryResultConverter.entity()); + } + + @SuppressWarnings({"ConstantConditions", "NullAway"}) + Stream doStream(Query query, Class entityType, String collectionName, Class returnType, + QueryResultConverter resultConverter) { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityType, "Entity type must not be null"); Assert.hasText(collectionName, "Collection name must not be null or empty"); Assert.notNull(returnType, "ReturnType must not be null"); - return execute(collectionName, (CollectionCallback>) collection -> { + return execute(collectionName, (CollectionCallback>) collection -> { MongoPersistentEntity persistentEntity = mappingContext.getPersistentEntity(entityType); @@ -501,8 +509,10 @@ protected Stream doStream(Query query, Class entityType, String collec FindIterable cursor = new QueryCursorPreparer(query, entityType).initiateFind(collection, col -> readPreference.prepare(col).find(mappedQuery, Document.class).projection(mappedFields)); + DocumentCallback resultReader = getResultReader(projection, collectionName, resultConverter); + return new CloseableIterableCursorAdapter<>(cursor, exceptionTranslator, - new ProjectingReadCallback<>(mongoConverter, projection, collectionName)).stream(); + resultReader).stream(); }); } @@ -512,7 +522,7 @@ public String getCollectionName(Class entityClass) { } @Override - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) public Document executeCommand(String jsonCommand) { Assert.hasText(jsonCommand, "JsonCommand must not be null nor empty"); @@ -521,7 +531,7 @@ public Document executeCommand(String jsonCommand) { } @Override - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) public Document executeCommand(Document command) { Assert.notNull(command, "Command must not be null"); @@ -530,7 +540,7 @@ public Document executeCommand(Document command) { } @Override - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) public Document executeCommand(Document command, @Nullable ReadPreference readPreference) { Assert.notNull(command, "Command must not be null"); @@ -577,7 +587,7 @@ protected void executeQuery(Query query, String collectionName, DocumentCallback } @Override - public T execute(DbCallback action) { + public @Nullable T execute(DbCallback action) { Assert.notNull(action, "DbCallback must not be null"); @@ -590,14 +600,14 @@ public T execute(DbCallback action) { } @Override - public T execute(Class entityClass, CollectionCallback callback) { + public @Nullable T execute(Class entityClass, CollectionCallback callback) { Assert.notNull(entityClass, "EntityClass must not be null"); return execute(getCollectionName(entityClass), callback); } @Override - public T execute(String collectionName, CollectionCallback callback) { + public @Nullable T execute(String collectionName, CollectionCallback callback) { Assert.notNull(collectionName, "CollectionName must not be null"); Assert.notNull(callback, "CollectionCallback must not be null"); @@ -619,6 +629,7 @@ public SessionScoped withSession(ClientSessionOptions options) { } @Override + @Contract("_ -> new") public MongoTemplate withSession(ClientSession session) { Assert.notNull(session, "ClientSession must not be null"); @@ -692,6 +703,7 @@ private MongoCollection createView(String name, String source, Aggrega return doCreateView(name, source, aggregation.getAggregationPipeline(), options); } + @SuppressWarnings("NullAway") protected MongoCollection doCreateView(String name, String source, List pipeline, @Nullable ViewOptions options) { @@ -707,8 +719,9 @@ protected MongoCollection doCreateView(String name, String source, Lis } @Override - @SuppressWarnings("ConstantConditions") - public MongoCollection getCollection(String collectionName) { + @SuppressWarnings({ "ConstantConditions", "NullAway" }) + @Contract("null -> fail") + public MongoCollection getCollection(@Nullable String collectionName) { Assert.notNull(collectionName, "CollectionName must not be null"); @@ -721,14 +734,14 @@ public boolean collectionExists(Class entityClass) { } @Override - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) public boolean collectionExists(String collectionName) { Assert.notNull(collectionName, "CollectionName must not be null"); return execute(db -> { - for (String name : MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db).listCollectionNames()) { + for (String name : db.listCollectionNames()) { if (name.equals(collectionName)) { return true; } @@ -855,7 +868,7 @@ public boolean exists(Query query, String collectionName) { } @Override - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) public boolean exists(Query query, @Nullable Class entityClass, String collectionName) { if (query == null) { @@ -898,10 +911,11 @@ public Window scroll(Query query, Class entityType) { @Override public Window scroll(Query query, Class entityType, String collectionName) { - return doScroll(query, entityType, entityType, collectionName); + return doScroll(query, entityType, entityType, QueryResultConverter.entity(), collectionName); } - Window doScroll(Query query, Class sourceClass, Class targetClass, String collectionName) { + Window doScroll(Query query, Class sourceClass, Class targetClass, + QueryResultConverter resultConverter, String collectionName) { Assert.notNull(query, "Query must not be null"); Assert.notNull(collectionName, "CollectionName must not be null"); @@ -909,7 +923,7 @@ Window doScroll(Query query, Class sourceClass, Class targetClass, Assert.notNull(targetClass, "Target type must not be null"); EntityProjection projection = operations.introspectProjection(targetClass, sourceClass); - ProjectingReadCallback callback = new ProjectingReadCallback<>(mongoConverter, projection, collectionName); + DocumentCallback callback = getResultReader(projection, collectionName, resultConverter); int limit = query.isLimited() ? query.getLimit() + 1 : Integer.MAX_VALUE; if (query.hasKeyset()) { @@ -917,14 +931,14 @@ Window doScroll(Query query, Class sourceClass, Class targetClass, KeysetScrollQuery keysetPaginationQuery = ScrollUtils.createKeysetPaginationQuery(query, operations.getIdPropertyName(sourceClass)); - List result = doFind(collectionName, createDelegate(query), keysetPaginationQuery.query(), + List result = doFind(collectionName, createDelegate(query), keysetPaginationQuery.query(), keysetPaginationQuery.fields(), sourceClass, new QueryCursorPreparer(query, keysetPaginationQuery.sort(), limit, 0, sourceClass), callback); return ScrollUtils.createWindow(query, result, sourceClass, operations); } - List result = doFind(collectionName, createDelegate(query), query.getQueryObject(), query.getFieldsObject(), + List result = doFind(collectionName, createDelegate(query), query.getQueryObject(), query.getFieldsObject(), sourceClass, new QueryCursorPreparer(query, query.getSortObject(), limit, query.getSkip(), sourceClass), callback); @@ -957,7 +971,7 @@ public List findDistinct(Query query, String field, Class entityClass, } @Override - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "NullAway" }) public List findDistinct(Query query, String field, String collectionName, Class entityClass, Class resultClass) { @@ -1016,6 +1030,11 @@ public GeoResults geoNear(NearQuery near, Class domainType, String col } public GeoResults geoNear(NearQuery near, Class domainType, String collectionName, Class returnType) { + return doGeoNear(near, domainType, collectionName, returnType, QueryResultConverter.entity()); + } + + GeoResults doGeoNear(NearQuery near, Class domainType, String collectionName, Class returnType, + QueryResultConverter resultConverter) { if (near == null) { throw new InvalidDataAccessApiUsageException("NearQuery must not be null"); @@ -1047,48 +1066,50 @@ public GeoResults geoNear(NearQuery near, Class domainType, String col AggregationResults results = aggregate($geoNear, collection, Document.class); EntityProjection projection = operations.introspectProjection(returnType, domainType); - DocumentCallback> callback = new GeoNearResultDocumentCallback<>(distanceField, - new ProjectingReadCallback<>(mongoConverter, projection, collection), near.getMetric()); + DocumentCallback> callback = new GeoNearResultDocumentCallback<>(distanceField, + getResultReader(projection, collectionName, resultConverter), near.getMetric()); - List> result = new ArrayList<>(results.getMappedResults().size()); + List> result = new ArrayList<>(results.getMappedResults().size()); BigDecimal aggregate = BigDecimal.ZERO; for (Document element : results) { - GeoResult geoResult = callback.doWith(element); + GeoResult geoResult = callback.doWith(element); aggregate = aggregate.add(BigDecimal.valueOf(geoResult.getDistance().getValue())); result.add(geoResult); } - Distance avgDistance = new Distance( + Distance avgDistance = Distance.of( result.size() == 0 ? 0 : aggregate.divide(new BigDecimal(result.size()), RoundingMode.HALF_UP).doubleValue(), near.getMetric()); return new GeoResults<>(result, avgDistance); } - @Nullable - @Override - public T findAndModify(Query query, UpdateDefinition update, Class entityClass) { + public @Nullable T findAndModify(Query query, UpdateDefinition update, Class entityClass) { return findAndModify(query, update, new FindAndModifyOptions(), entityClass, getCollectionName(entityClass)); } - @Nullable @Override - public T findAndModify(Query query, UpdateDefinition update, Class entityClass, String collectionName) { + public @Nullable T findAndModify(Query query, UpdateDefinition update, Class entityClass, + String collectionName) { return findAndModify(query, update, new FindAndModifyOptions(), entityClass, collectionName); } - @Nullable @Override - public T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass) { + public @Nullable T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, + Class entityClass) { return findAndModify(query, update, options, entityClass, getCollectionName(entityClass)); } - @Nullable @Override - public T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass, - String collectionName) { + public @Nullable T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, + Class entityClass, String collectionName) { + return findAndModify(query, update, options, entityClass, collectionName, QueryResultConverter.entity()); + } + + @Nullable T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, + Class entityClass, String collectionName, QueryResultConverter resultConverter) { Assert.notNull(query, "Query must not be null"); Assert.notNull(update, "Update must not be null"); @@ -1108,12 +1129,17 @@ public T findAndModify(Query query, UpdateDefinition update, FindAndModifyOp } return doFindAndModify(createDelegate(query), collectionName, query.getQueryObject(), query.getFieldsObject(), - getMappedSortObject(query, entityClass), entityClass, update, optionsToUse); + getMappedSortObject(query, entityClass), entityClass, update, optionsToUse, resultConverter); } @Override - public T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class entityType, - String collectionName, Class resultType) { + public @Nullable T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, + Class entityType, String collectionName, Class resultType) { + return findAndReplace(query, replacement, options, entityType, collectionName, resultType, QueryResultConverter.entity()); + } + + @Nullable R findAndReplace(Query query, S replacement, FindAndReplaceOptions options, + Class entityType, String collectionName, Class resultType, QueryResultConverter resultConverter) { Assert.notNull(query, "Query must not be null"); Assert.notNull(replacement, "Replacement must not be null"); @@ -1140,8 +1166,8 @@ public T findAndReplace(Query query, S replacement, FindAndReplaceOptions maybeEmitEvent(new BeforeSaveEvent<>(replacement, mappedReplacement, collectionName)); maybeCallBeforeSave(replacement, mappedReplacement, collectionName); - T saved = doFindAndReplace(collectionPreparer, collectionName, mappedQuery, mappedFields, mappedSort, - queryContext.getCollation(entityType).orElse(null), entityType, mappedReplacement, options, projection); + R saved = doFindAndReplace(collectionPreparer, collectionName, mappedQuery, mappedFields, mappedSort, + queryContext.getCollation(entityType).orElse(null), entityType, mappedReplacement, options, projection, resultConverter); if (saved != null) { maybeEmitEvent(new AfterSaveEvent<>(saved, mappedReplacement, collectionName)); @@ -1154,15 +1180,13 @@ public T findAndReplace(Query query, S replacement, FindAndReplaceOptions // Find methods that take a Query to express the query and that return a single object that is also removed from the // collection in the database. - @Nullable @Override - public T findAndRemove(Query query, Class entityClass) { + public @Nullable T findAndRemove(Query query, Class entityClass) { return findAndRemove(query, entityClass, getCollectionName(entityClass)); } - @Nullable @Override - public T findAndRemove(Query query, Class entityClass, String collectionName) { + public @Nullable T findAndRemove(Query query, Class entityClass, String collectionName) { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityClass, "EntityClass must not be null"); @@ -1224,6 +1248,7 @@ public long estimatedCount(String collectionName) { return doEstimatedCount(CollectionPreparerDelegate.of(this), collectionName, new EstimatedDocumentCountOptions()); } + @SuppressWarnings("NullAway") protected long doEstimatedCount(CollectionPreparer> collectionPreparer, String collectionName, EstimatedDocumentCountOptions options) { return execute(collectionName, @@ -1241,6 +1266,7 @@ public long exactCount(Query query, @Nullable Class entityClass, String colle return doExactCount(createDelegate(query), collectionName, mappedQuery, options); } + @SuppressWarnings("NullAway") protected long doExactCount(CollectionPreparer> collectionPreparer, String collectionName, Document filter, CountOptions options) { return execute(collectionName, collection -> collectionPreparer.prepare(collection) @@ -1433,7 +1459,10 @@ protected Collection doInsertBatch(String collectionName, Collection(initialized, document, collectionName)); initialized = maybeCallBeforeSave(initialized, document, collectionName); - documentList.add(document); + MappedDocument mappedDocument = queryOperations.createInsertContext(MappedDocument.of(document)) + .prepareId(uninitialized.getClass()); + + documentList.add(mappedDocument.getDocument()); initializedBatchToSave.add(initialized); } @@ -1541,7 +1570,7 @@ protected T doSave(String collectionName, T objectToSave, MongoWriter wri return maybeCallAfterSave(saved, dbDoc, collectionName); } - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) protected Object insertDocument(String collectionName, Document document, Class entityClass) { if (LOGGER.isDebugEnabled()) { @@ -1595,6 +1624,7 @@ protected List insertDocumentList(String collectionName, List return MappedDocument.toIds(documents); } + @SuppressWarnings("NullAway") protected Object saveDocument(String collectionName, Document dbDoc, Class entityClass) { if (LOGGER.isDebugEnabled()) { @@ -1693,7 +1723,7 @@ public UpdateResult updateMulti(Query query, UpdateDefinition update, Class e return doUpdate(collectionName, query, update, entityClass, false, true); } - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) protected UpdateResult doUpdate(String collectionName, Query query, UpdateDefinition update, @Nullable Class entityClass, boolean upsert, boolean multi) { @@ -1803,7 +1833,7 @@ public DeleteResult remove(Query query, Class entityClass, String collectionN return doRemove(collectionName, query, entityClass, true); } - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) protected DeleteResult doRemove(String collectionName, Query query, @Nullable Class entityClass, boolean multi) { @@ -1977,11 +2007,6 @@ public List mapReduce(Query query, Class domainType, String inputColle mapReduce = mapReduce.jsMode(mapReduceOptions.getJavaScriptMode()); } - if (mapReduceOptions.getOutputSharded().isPresent()) { - MongoCompatibilityAdapter.mapReduceIterableAdapter(mapReduce) - .sharded(mapReduceOptions.getOutputSharded().get()); - } - if (StringUtils.hasText(mapReduceOptions.getOutputCollection()) && !mapReduceOptions.usesInlineOutput()) { mapReduce = mapReduce.collectionName(mapReduceOptions.getOutputCollection()) @@ -2019,7 +2044,7 @@ public AggregationResults aggregate(TypedAggregation aggregation, Clas @Override public AggregationResults aggregate(TypedAggregation aggregation, String inputCollectionName, Class outputType) { - return aggregate(aggregation, inputCollectionName, outputType, null); + return aggregate(aggregation, inputCollectionName, outputType, (AggregationOperationContext) null); } @Override @@ -2032,7 +2057,7 @@ public AggregationResults aggregate(Aggregation aggregation, Class inp @Override public AggregationResults aggregate(Aggregation aggregation, String collectionName, Class outputType) { - return aggregate(aggregation, collectionName, outputType, null); + return doAggregate(aggregation, collectionName, outputType, QueryResultConverter.entity()); } @Override @@ -2130,17 +2155,39 @@ protected UpdateResult replace(Query query, Class entityType, T replac * @return */ protected List doFindAndDelete(String collectionName, Query query, Class entityClass) { + return doFindAndDelete(collectionName, query, entityClass, QueryResultConverter.entity()); + } - List result = find(query, entityClass, collectionName); + @SuppressWarnings("NullAway") + List doFindAndDelete(String collectionName, Query query, Class entityClass, + QueryResultConverter resultConverter) { + + List ids = new ArrayList<>(); + + QueryResultConverterCallback callback = new QueryResultConverterCallback<>(resultConverter, + new ProjectingReadCallback<>(getConverter(), EntityProjection.nonProjecting(entityClass), collectionName)) { + @Override + public T doWith(Document object) { + ids.add(object.get("_id")); + return super.doWith(object); + } + }; + + List result = doFind(collectionName, createDelegate(query), query.getQueryObject(), query.getFieldsObject(), entityClass, + new QueryCursorPreparer(query, entityClass), callback); if (!CollectionUtils.isEmpty(result)) { - Query byIdInQuery = operations.getByIdInQuery(result); + Criteria[] criterias = ids.stream() // + .map(it -> Criteria.where("_id").is(it)) // + .toArray(Criteria[]::new); + + Query removeQuery = new Query(criterias.length == 1 ? criterias[0] : new Criteria().orOperator(criterias)); if (query.hasReadPreference()) { - byIdInQuery.withReadPreference(query.getReadPreference()); + removeQuery.withReadPreference(query.getReadPreference()); } - remove(byIdInQuery, entityClass, collectionName); + remove(removeQuery, entityClass, collectionName); } return result; @@ -2162,11 +2209,25 @@ private AggregationResults doAggregate(Aggregation aggregation, String co return doAggregate(aggregation, collectionName, outputType, context.getAggregationOperationContext()); } - @SuppressWarnings("ConstantConditions") + AggregationResults doAggregate(Aggregation aggregation, String collectionName, Class outputType, + QueryResultConverter resultConverter) { + + return doAggregate(aggregation, collectionName, outputType, resultConverter, queryOperations + .createAggregation(aggregation, (AggregationOperationContext) null).getAggregationOperationContext()); + } + + @SuppressWarnings({ "ConstantConditions", "NullAway" }) protected AggregationResults doAggregate(Aggregation aggregation, String collectionName, Class outputType, AggregationOperationContext context) { + return doAggregate(aggregation, collectionName, outputType, QueryResultConverter.entity(), context); + } + + @SuppressWarnings({"ConstantConditions", "NullAway"}) + AggregationResults doAggregate(Aggregation aggregation, String collectionName, Class outputType, + QueryResultConverter resultConverter, AggregationOperationContext context) { - ReadDocumentCallback callback = new ReadDocumentCallback<>(mongoConverter, outputType, collectionName); + DocumentCallback callback = new QueryResultConverterCallback<>(resultConverter, + new ReadDocumentCallback<>(mongoConverter, outputType, collectionName)); AggregationOptions options = aggregation.getOptions(); AggregationUtil aggregationUtil = new AggregationUtil(queryMapper, mappingContext); @@ -2245,9 +2306,15 @@ protected AggregationResults doAggregate(Aggregation aggregation, String }); } - @SuppressWarnings("ConstantConditions") protected Stream aggregateStream(Aggregation aggregation, String collectionName, Class outputType, @Nullable AggregationOperationContext context) { + return doAggregateStream(aggregation, collectionName, outputType, QueryResultConverter.entity(), context); + } + + @SuppressWarnings({ "ConstantConditions", "NullAway" }) + Stream doAggregateStream(Aggregation aggregation, String collectionName, Class outputType, + QueryResultConverter resultConverter, + @Nullable AggregationOperationContext context) { Assert.notNull(aggregation, "Aggregation pipeline must not be null"); Assert.hasText(collectionName, "Collection name must not be null or empty"); @@ -2264,7 +2331,8 @@ protected Stream aggregateStream(Aggregation aggregation, String collecti String.format("Streaming aggregation: %s in collection %s", serializeToJsonSafely(pipeline), collectionName)); } - ReadDocumentCallback readCallback = new ReadDocumentCallback<>(mongoConverter, outputType, collectionName); + DocumentCallback readCallback = new QueryResultConverterCallback<>(resultConverter, + new ReadDocumentCallback<>(mongoConverter, outputType, collectionName)); return execute(collectionName, (CollectionCallback>) collection -> { @@ -2290,7 +2358,8 @@ protected Stream aggregateStream(Aggregation aggregation, String collecti cursor = cursor.maxTime(options.getMaxTime().toMillis(), TimeUnit.MILLISECONDS); } - Class domainType = aggregation instanceof TypedAggregation typedAggregation ? typedAggregation.getInputType() + Class domainType = aggregation instanceof TypedAggregation typedAggregation + ? typedAggregation.getInputType() : null; Optionals.firstNonEmpty(options::getCollation, // @@ -2360,11 +2429,11 @@ protected String replaceWithResourceIfNecessary(String function) { } @Override - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) public Set getCollectionNames() { return execute(db -> { Set result = new LinkedHashSet<>(); - for (String name : MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db).listCollectionNames()) { + for (String name : db.listCollectionNames()) { result.add(name); } return result; @@ -2444,7 +2513,7 @@ protected MongoCollection doCreateCollection(String collectionName, Do * @return the collection that was created * @since 3.3.3 */ - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) protected MongoCollection doCreateCollection(String collectionName, CreateCollectionOptions collectionOptions) { @@ -2523,8 +2592,9 @@ private CreateCollectionOptions getCreateCollectionOptions(Document document) { * @return the converted object or {@literal null} if none exists. */ @Nullable - protected T doFindOne(String collectionName, CollectionPreparer> collectionPreparer, - Document query, Document fields, Class entityClass) { + protected T doFindOne(String collectionName, + CollectionPreparer> collectionPreparer, Document query, Document fields, + Class entityClass) { return doFindOne(collectionName, collectionPreparer, query, fields, CursorPreparer.NO_OP_PREPARER, entityClass); } @@ -2543,8 +2613,9 @@ protected T doFindOne(String collectionName, CollectionPreparer T doFindOne(String collectionName, CollectionPreparer> collectionPreparer, - Document query, Document fields, CursorPreparer preparer, Class entityClass) { + protected T doFindOne(String collectionName, + CollectionPreparer> collectionPreparer, Document query, Document fields, + CursorPreparer preparer, Class entityClass) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); @@ -2610,7 +2681,9 @@ protected List doFind(String collectionName, if (LOGGER.isDebugEnabled()) { - Document mappedSort = preparer instanceof SortingQueryCursorPreparer sqcp ? getMappedSortObject(sqcp.getSortObject(), entity) : null; + Document mappedSort = preparer instanceof SortingQueryCursorPreparer sqcp + ? getMappedSortObject(sqcp.getSortObject(), entity) + : null; LOGGER.debug(String.format("find using query: %s fields: %s sort: %s for class: %s in collection: %s", serializeToJsonSafely(mappedQuery), mappedFields, serializeToJsonSafely(mappedSort), entityClass, collectionName)); @@ -2626,11 +2699,12 @@ protected List doFind(String collectionName, * * @since 2.0 */ - List doFind(CollectionPreparer> collectionPreparer, String collectionName, - Document query, Document fields, Class sourceClass, Class targetClass, CursorPreparer preparer) { + List doFind(CollectionPreparer> collectionPreparer, String collectionName, + Document query, Document fields, Class sourceClass, Class targetClass, + QueryResultConverter resultConverter, CursorPreparer preparer) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(sourceClass); - EntityProjection projection = operations.introspectProjection(targetClass, sourceClass); + EntityProjection projection = operations.introspectProjection(targetClass, sourceClass); QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields)); Document mappedFields = queryContext.getMappedFields(entity, projection); @@ -2646,8 +2720,9 @@ List doFind(CollectionPreparer> collectionPr collectionName)); } + DocumentCallback callback = getResultReader(projection, collectionName, resultConverter); return executeFindMultiInternal(new FindCallback(collectionPreparer, mappedQuery, mappedFields, null), preparer, - new ProjectingReadCallback<>(mongoConverter, projection, collectionName), collectionName); + callback, collectionName); } /** @@ -2720,8 +2795,9 @@ Document getMappedValidator(Validator validator, Class domainType) { * @return the List of converted objects. */ @SuppressWarnings("ConstantConditions") - protected T doFindAndRemove(CollectionPreparer collectionPreparer, String collectionName, Document query, - Document fields, Document sort, @Nullable Collation collation, Class entityClass) { + protected @Nullable T doFindAndRemove(CollectionPreparer collectionPreparer, String collectionName, + Document query, @Nullable Document fields, @Nullable Document sort, @Nullable Collation collation, + Class entityClass) { if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("findAndRemove using query: %s fields: %s sort: %s for class: %s in collection: %s", @@ -2736,9 +2812,10 @@ protected T doFindAndRemove(CollectionPreparer collectionPreparer, String co } @SuppressWarnings("ConstantConditions") - protected T doFindAndModify(CollectionPreparer collectionPreparer, String collectionName, Document query, - Document fields, Document sort, Class entityClass, UpdateDefinition update, - @Nullable FindAndModifyOptions options) { + @Nullable T doFindAndModify(CollectionPreparer> collectionPreparer, + String collectionName, + Document query, @Nullable Document fields, @Nullable Document sort, Class entityClass, UpdateDefinition update, + @Nullable FindAndModifyOptions options, QueryResultConverter resultConverter) { if (options == null) { options = new FindAndModifyOptions(); @@ -2760,10 +2837,12 @@ protected T doFindAndModify(CollectionPreparer collectionPreparer, String co serializeToJsonSafely(mappedUpdate), collectionName)); } + DocumentCallback callback = getResultReader(EntityProjection.nonProjecting(entityClass), collectionName, resultConverter); + return executeFindOneInternal( new FindAndModifyCallback(collectionPreparer, mappedQuery, fields, sort, mappedUpdate, update.getArrayFilters().stream().map(ArrayFilter::asDocument).collect(Collectors.toList()), options), - new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName); + callback, collectionName); } /** @@ -2782,14 +2861,16 @@ protected T doFindAndModify(CollectionPreparer collectionPreparer, String co * {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}. */ @Nullable - protected T doFindAndReplace(CollectionPreparer collectionPreparer, String collectionName, Document mappedQuery, - Document mappedFields, Document mappedSort, @Nullable com.mongodb.client.model.Collation collation, - Class entityType, Document replacement, FindAndReplaceOptions options, Class resultType) { + protected T doFindAndReplace(CollectionPreparer> collectionPreparer, + String collectionName, + Document mappedQuery, Document mappedFields, Document mappedSort, + com.mongodb.client.model.@Nullable Collation collation, Class entityType, Document replacement, + FindAndReplaceOptions options, Class resultType) { - EntityProjection projection = operations.introspectProjection(resultType, entityType); + EntityProjection projection = operations.introspectProjection(resultType, entityType); return doFindAndReplace(collectionPreparer, collectionName, mappedQuery, mappedFields, mappedSort, collation, - entityType, replacement, options, projection); + entityType, replacement, options, projection, QueryResultConverter.entity()); } CollectionPreparerDelegate createDelegate(Query query) { @@ -2824,9 +2905,11 @@ CollectionPreparer> createCollectionPreparer(Query que * @since 3.4 */ @Nullable - private T doFindAndReplace(CollectionPreparer collectionPreparer, String collectionName, Document mappedQuery, - Document mappedFields, Document mappedSort, @Nullable com.mongodb.client.model.Collation collation, - Class entityType, Document replacement, FindAndReplaceOptions options, EntityProjection projection) { + private R doFindAndReplace(CollectionPreparer> collectionPreparer, + String collectionName, + Document mappedQuery, Document mappedFields, Document mappedSort, + com.mongodb.client.model.@Nullable Collation collation, Class entityType, Document replacement, + FindAndReplaceOptions options, EntityProjection projection, QueryResultConverter resultConverter) { if (LOGGER.isDebugEnabled()) { LOGGER @@ -2837,11 +2920,13 @@ private T doFindAndReplace(CollectionPreparer collectionPreparer, String col serializeToJsonSafely(mappedSort), entityType, serializeToJsonSafely(replacement), collectionName)); } + DocumentCallback callback = getResultReader(projection, collectionName, resultConverter); return executeFindOneInternal(new FindAndReplaceCallback(collectionPreparer, mappedQuery, mappedFields, mappedSort, - replacement, collation, options), new ProjectingReadCallback<>(mongoConverter, projection, collectionName), + replacement, collation, options),callback, collectionName); } + @SuppressWarnings("NullAway") private UpdateResult doReplace(ReplaceOptions options, Class entityType, String collectionName, UpdateContext updateContext, CollectionPreparer> collectionPreparer, Document replacement) { @@ -2966,6 +3051,16 @@ private void executeQueryInternal(CollectionCallback> col } } + @SuppressWarnings("unchecked") + private DocumentCallback getResultReader(EntityProjection projection, String collectionName, + QueryResultConverter resultConverter) { + + DocumentCallback readCallback = new ProjectingReadCallback<>(mongoConverter, projection, collectionName); + + return resultConverter == QueryResultConverter.entity() ? (DocumentCallback) readCallback + : new QueryResultConverterCallback(resultConverter, readCallback); + } + public PersistenceExceptionTranslator getExceptionTranslator() { return exceptionTranslator; } @@ -3002,13 +3097,12 @@ private Document getMappedSortObject(@Nullable Query query, Class type) { return getMappedSortObject(query.getSortObject(), type); } - @Nullable - private Document getMappedSortObject(Document sortObject, Class type) { + private @Nullable Document getMappedSortObject(@Nullable Document sortObject, Class type) { return getMappedSortObject(sortObject, mappingContext.getPersistentEntity(type)); } - @Nullable - private Document getMappedSortObject(Document sortObject, @Nullable MongoPersistentEntity entity) { + + private @Nullable Document getMappedSortObject(@Nullable Document sortObject, @Nullable MongoPersistentEntity entity) { if (ObjectUtils.isEmpty(sortObject)) { return null; @@ -3084,10 +3178,10 @@ private static class FindCallback implements CollectionCallback> collectionPreparer; private final Document query; private final Document fields; - private final @Nullable com.mongodb.client.model.Collation collation; + private final com.mongodb.client.model.@Nullable Collation collation; public FindCallback(CollectionPreparer> collectionPreparer, Document query, - Document fields, @Nullable com.mongodb.client.model.Collation collation) { + Document fields, com.mongodb.client.model.@Nullable Collation collation) { Assert.notNull(query, "Query must not be null"); Assert.notNull(fields, "Fields must not be null"); @@ -3123,10 +3217,10 @@ private class ExistsCallback implements CollectionCallback { private final CollectionPreparer collectionPreparer; private final Document mappedQuery; - private final com.mongodb.client.model.Collation collation; + private final com.mongodb.client.model.@Nullable Collation collation; ExistsCallback(CollectionPreparer collectionPreparer, Document mappedQuery, - com.mongodb.client.model.Collation collation) { + com.mongodb.client.model.@Nullable Collation collation) { this.collectionPreparer = collectionPreparer; this.mappedQuery = mappedQuery; @@ -3151,12 +3245,12 @@ private static class FindAndRemoveCallback implements CollectionCallback> collectionPreparer; private final Document query; - private final Document fields; - private final Document sort; + private final @Nullable Document fields; + private final @Nullable Document sort; private final Optional collation; FindAndRemoveCallback(CollectionPreparer> collectionPreparer, Document query, - Document fields, Document sort, @Nullable Collation collation) { + @Nullable Document fields, @Nullable Document sort, @Nullable Collation collation) { this.collectionPreparer = collectionPreparer; this.query = query; @@ -3179,14 +3273,15 @@ private static class FindAndModifyCallback implements CollectionCallback> collectionPreparer; private final Document query; - private final Document fields; - private final Document sort; + private final @Nullable Document fields; + private final @Nullable Document sort; private final Object update; private final List arrayFilters; private final FindAndModifyOptions options; FindAndModifyCallback(CollectionPreparer> collectionPreparer, Document query, - Document fields, Document sort, Object update, List arrayFilters, FindAndModifyOptions options) { + @Nullable Document fields, @Nullable Document sort, Object update, List arrayFilters, + FindAndModifyOptions options) { this.collectionPreparer = collectionPreparer; this.query = query; @@ -3240,11 +3335,11 @@ private static class FindAndReplaceCallback implements CollectionCallback> collectionPreparer, Document query, - Document fields, Document sort, Document update, @Nullable com.mongodb.client.model.Collation collation, + Document fields, Document sort, Document update, com.mongodb.client.model.@Nullable Collation collation, FindAndReplaceOptions options) { this.collectionPreparer = collectionPreparer; this.query = query; @@ -3325,6 +3420,24 @@ public T doWith(Document document) { } } + static class QueryResultConverterCallback implements DocumentCallback { + + private final QueryResultConverter converter; + private final DocumentCallback delegate; + + QueryResultConverterCallback(QueryResultConverter converter, DocumentCallback delegate) { + this.converter = converter; + this.delegate = delegate; + } + + @Override + public R doWith(Document object) { + + Lazy lazy = Lazy.of(() -> delegate.doWith(object)); + return converter.mapDocument(object, lazy::get); + } + } + /** * {@link DocumentCallback} transforming {@link Document} into the given {@code targetType} or decorating the * {@code sourceType} with a {@literal projection} in case the {@code targetType} is an {@literal interface}. @@ -3350,10 +3463,6 @@ private class ProjectingReadCallback implements DocumentCallback { @SuppressWarnings("unchecked") public T doWith(Document document) { - if (document == null) { - return null; - } - maybeEmitEvent(new AfterLoadEvent<>(document, projection.getMappedType().getType(), collectionName)); Object entity = mongoConverter.project(projection, document); @@ -3509,7 +3618,7 @@ public GeoResult doWith(Document object) { T doWith = delegate.doWith(object); - return new GeoResult<>(doWith, new Distance(distance, metric)); + return new GeoResult<>(doWith, Distance.of(distance, metric)); } } @@ -3598,8 +3707,6 @@ public void close() { throw potentiallyConvertRuntimeException(ex, exceptionTranslator); } finally { cursor = null; - exceptionTranslator = null; - objectReadCallback = null; } } } @@ -3631,7 +3738,7 @@ static class SessionBoundMongoTemplate extends MongoTemplate { } @Override - public MongoCollection getCollection(String collectionName) { + public MongoCollection getCollection(@Nullable String collectionName) { // native MongoDB objects that offer methods with ClientSession must not be proxied. return delegate.getCollection(collectionName); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java index 28ca85fbd7..4ae618eaa1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java @@ -31,6 +31,7 @@ import org.bson.codecs.Codec; import org.bson.conversions.Bson; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.PropertyReferenceException; import org.springframework.data.mapping.context.MappingContext; @@ -62,7 +63,7 @@ import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.projection.EntityProjection; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import com.mongodb.client.model.CountOptions; @@ -283,6 +284,7 @@ MappedDocument prepareId(Class type) { * @param * @return the {@link MappedDocument} containing the changes. */ + @SuppressWarnings("NullAway") MappedDocument prepareId(@Nullable MongoPersistentEntity entity) { if (entity == null || source.hasId()) { @@ -361,6 +363,7 @@ Document getMappedQuery(@Nullable MongoPersistentEntity entity) { return queryMapper.getMappedObject(getQueryObject(), entity); } + @SuppressWarnings("NullAway") Document getMappedFields(@Nullable MongoPersistentEntity entity, EntityProjection projection) { Document fields = evaluateFields(entity); @@ -888,6 +891,8 @@ Document getMappedShardKey(MongoPersistentEntity entity) { */ List getUpdatePipeline(@Nullable Class domainType) { + Assert.isInstanceOf(AggregationUpdate.class, update); + Class type = domainType != null ? domainType : Object.class; AggregationOperationContext context = new RelaxedTypeBasedAggregationOperationContext(type, mappingContext, @@ -901,6 +906,7 @@ List getUpdatePipeline(@Nullable Class domainType) { * @param entity * @return */ + @SuppressWarnings("NullAway") Document getMappedUpdate(@Nullable MongoPersistentEntity entity) { if (update != null) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryResultConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryResultConverter.java new file mode 100644 index 0000000000..ca93940a9c --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryResultConverter.java @@ -0,0 +1,85 @@ +/* + * 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.mongodb.core; + +import org.bson.Document; + +/** + * Converter for MongoDB query results. + *

+ * This is a functional interface that allows for mapping a {@link Document} to a result type. + * {@link #mapDocument(Document, ConversionResultSupplier) row mapping} can obtain upstream a + * {@link ConversionResultSupplier upstream converter} to enrich the final result object. This is useful when e.g. + * wrapping result objects where the wrapper needs to obtain information from the actual {@link Document}. + * + * @param object type accepted by this converter. + * @param the returned result type. + * @author Mark Paluch + * @since 5.0 + */ +@FunctionalInterface +public interface QueryResultConverter { + + /** + * Returns a function that returns the materialized entity. + * + * @param the type of the input and output entity to the function. + * @return a function that returns the materialized entity. + */ + @SuppressWarnings("unchecked") + static QueryResultConverter entity() { + return (QueryResultConverter) EntityResultConverter.INSTANCE; + } + + /** + * Map a {@link Document} that is read from the MongoDB query/aggregation operation to a query result. + * + * @param document the raw document from the MongoDB query/aggregation result. + * @param reader reader object that supplies an upstream result from an earlier converter. + * @return the mapped result. + */ + R mapDocument(Document document, ConversionResultSupplier reader); + + /** + * Returns a composed function that first applies this function to its input, and then applies the {@code after} + * function to the result. If evaluation of either function throws an exception, it is relayed to the caller of the + * composed function. + * + * @param the type of output of the {@code after} function, and of the composed function. + * @param after the function to apply after this function is applied. + * @return a composed function that first applies this function and then applies the {@code after} function. + */ + default QueryResultConverter andThen(QueryResultConverter after) { + return (row, reader) -> after.mapDocument(row, () -> mapDocument(row, reader)); + } + + /** + * A supplier that converts a {@link Document} into {@code T}. Allows for lazy reading of query results. + * + * @param type of the returned result. + */ + interface ConversionResultSupplier { + + /** + * Obtain the upstream conversion result. + * + * @return the upstream conversion result. + */ + T get(); + + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperation.java index 54129e6b5d..99c94b19e4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperation.java @@ -18,6 +18,7 @@ import reactor.core.publisher.Flux; import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.lang.Contract; /** * {@link ReactiveAggregationOperation} allows creation and execution of reactive MongoDB aggregation operations in a @@ -44,7 +45,7 @@ public interface ReactiveAggregationOperation { /** * Start creating an aggregation operation that returns results mapped to the given domain type.
* Use {@link org.springframework.data.mongodb.core.aggregation.TypedAggregation} to specify a potentially different - * input type for he aggregation. + * input type for the aggregation. * * @param domainType must not be {@literal null}. * @return new instance of {@link ReactiveAggregation}. Never {@literal null}. @@ -73,6 +74,18 @@ interface AggregationOperationWithCollection { */ interface TerminatingAggregationOperation { + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link ExecutableFindOperation.TerminatingFindNear}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since 5.0 + */ + @Contract("_ -> new") + TerminatingAggregationOperation map(QueryResultConverter converter); + /** * Apply pipeline operations as specified and stream all matching elements.
* diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupport.java index 954fd61716..fbaff2bc39 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupport.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Flux; import org.springframework.data.mongodb.core.aggregation.Aggregation; @@ -51,22 +52,25 @@ public ReactiveAggregation aggregateAndReturn(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); - return new ReactiveAggregationSupport<>(template, domainType, null, null); + return new ReactiveAggregationSupport<>(template, domainType, QueryResultConverter.entity(), null, null); } - static class ReactiveAggregationSupport + static class ReactiveAggregationSupport implements AggregationOperationWithAggregation, ReactiveAggregation, TerminatingAggregationOperation { private final ReactiveMongoTemplate template; - private final Class domainType; - private final Aggregation aggregation; - private final String collection; + private final Class domainType; + private final QueryResultConverter resultConverter; + private final @Nullable Aggregation aggregation; + private final @Nullable String collection; - ReactiveAggregationSupport(ReactiveMongoTemplate template, Class domainType, Aggregation aggregation, - String collection) { + ReactiveAggregationSupport(ReactiveMongoTemplate template, Class domainType, + QueryResultConverter resultConverter, @Nullable Aggregation aggregation, + @Nullable String collection) { this.template = template; this.domainType = domainType; + this.resultConverter = resultConverter; this.aggregation = aggregation; this.collection = collection; } @@ -76,7 +80,7 @@ public AggregationOperationWithAggregation inCollection(String collection) { Assert.hasText(collection, "Collection must not be null nor empty"); - return new ReactiveAggregationSupport<>(template, domainType, aggregation, collection); + return new ReactiveAggregationSupport<>(template, domainType, resultConverter, aggregation, collection); } @Override @@ -84,12 +88,24 @@ public TerminatingAggregationOperation by(Aggregation aggregation) { Assert.notNull(aggregation, "Aggregation must not be null"); - return new ReactiveAggregationSupport<>(template, domainType, aggregation, collection); + return new ReactiveAggregationSupport<>(template, domainType, resultConverter, aggregation, collection); + } + + @Override + public TerminatingAggregationOperation map(QueryResultConverter converter) { + + Assert.notNull(converter, "QueryResultConverter must not be null"); + + return new ReactiveAggregationSupport<>(template, domainType, resultConverter.andThen(converter), aggregation, + collection); } @Override public Flux all() { - return template.aggregate(aggregation, getCollectionName(aggregation), domainType); + + Assert.notNull(aggregation, "Aggregation must be set first"); + + return template.doAggregate(aggregation, getCollectionName(aggregation), domainType, domainType, resultConverter); } private String getCollectionName(Aggregation aggregation) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveChangeStreamOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveChangeStreamOperationSupport.java index afeb6c5e0e..589f264f17 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveChangeStreamOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveChangeStreamOperationSupport.java @@ -24,11 +24,11 @@ import org.bson.BsonTimestamp; import org.bson.BsonValue; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.ChangeStreamOptions.ChangeStreamOptionsBuilder; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.MatchOperation; import org.springframework.data.mongodb.core.query.CriteriaDefinition; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java index cba827ffed..eaa9da4a37 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java @@ -25,6 +25,7 @@ import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.lang.Contract; /** * {@link ReactiveFindOperation} allows creation and execution of reactive MongoDB find operations in a fluent API @@ -66,7 +67,28 @@ public interface ReactiveFindOperation { /** * Compose find execution by calling one of the terminating methods. */ - interface TerminatingFind { + interface TerminatingFind extends TerminatingResults, TerminatingProjection { + + } + + /** + * Compose find execution by calling one of the terminating methods. + * + * @since 5.0 + */ + interface TerminatingResults { + + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link TerminatingResults}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since 5.0 + */ + @Contract("_ -> new") + TerminatingResults map(QueryResultConverter converter); /** * Get exactly zero or one result. @@ -95,7 +117,7 @@ interface TerminatingFind { *

* When using {@link KeysetScrollPosition}, make sure to use non-nullable * {@link org.springframework.data.domain.Sort sort properties} as MongoDB does not support criteria to reconstruct - * a query result from absent document fields or {@code null} values through {@code $gt/$lt} operators. + * a query result from absent document fields or {@literal null} values through {@code $gt/$lt} operators. * * @param scrollPosition the scroll position. * @return a scroll of the resulting elements. @@ -120,6 +142,15 @@ interface TerminatingFind { */ Flux tail(); + } + + /** + * Compose find execution by calling one of the terminating methods. + * + * @since 5.0 + */ + interface TerminatingProjection { + /** * Get the number of matching elements.
* This method uses an @@ -145,6 +176,18 @@ interface TerminatingFind { */ interface TerminatingFindNear { + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link ExecutableFindOperation.TerminatingFindNear}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since 5.0 + */ + @Contract("_ -> new") + TerminatingFindNear map(QueryResultConverter converter); + /** * Find all matching elements and return them as {@link org.springframework.data.geo.GeoResult}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java index d1aec8af36..38e32dc977 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java @@ -19,14 +19,15 @@ import reactor.core.publisher.Mono; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.dao.IncorrectResultSizeDataAccessException; -import org.springframework.data.domain.Window; import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Window; +import org.springframework.data.geo.GeoResult; import org.springframework.data.mongodb.core.CollectionPreparerSupport.ReactiveCollectionPreparerDelegate; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.SerializationUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -52,7 +53,7 @@ public ReactiveFind query(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); - return new ReactiveFindSupport<>(template, domainType, domainType, null, ALL_QUERY); + return new ReactiveFindSupport<>(template, domainType, domainType, QueryResultConverter.entity(), null, ALL_QUERY); } /** @@ -61,21 +62,24 @@ public ReactiveFind query(Class domainType) { * @author Christoph Strobl * @since 2.0 */ - static class ReactiveFindSupport + static class ReactiveFindSupport implements ReactiveFind, FindWithCollection, FindWithProjection, FindWithQuery { private final ReactiveMongoTemplate template; private final Class domainType; - private final Class returnType; - private final String collection; + private final Class returnType; + private final QueryResultConverter resultConverter; + private final @Nullable String collection; private final Query query; - ReactiveFindSupport(ReactiveMongoTemplate template, Class domainType, Class returnType, String collection, + ReactiveFindSupport(ReactiveMongoTemplate template, Class domainType, Class returnType, + QueryResultConverter resultConverter, @Nullable String collection, Query query) { this.template = template; this.domainType = domainType; this.returnType = returnType; + this.resultConverter = resultConverter; this.collection = collection; this.query = query; } @@ -85,7 +89,7 @@ public FindWithProjection inCollection(String collection) { Assert.hasText(collection, "Collection name must not be null nor empty"); - return new ReactiveFindSupport<>(template, domainType, returnType, collection, query); + return new ReactiveFindSupport<>(template, domainType, returnType, resultConverter, collection, query); } @Override @@ -93,7 +97,8 @@ public FindWithQuery as(Class returnType) { Assert.notNull(returnType, "ReturnType must not be null"); - return new ReactiveFindSupport<>(template, domainType, returnType, collection, query); + return new ReactiveFindSupport<>(template, domainType, returnType, QueryResultConverter.entity(), collection, + query); } @Override @@ -101,7 +106,16 @@ public TerminatingFind matching(Query query) { Assert.notNull(query, "Query must not be null"); - return new ReactiveFindSupport<>(template, domainType, returnType, collection, query); + return new ReactiveFindSupport<>(template, domainType, returnType, resultConverter, collection, query); + } + + @Override + public TerminatingResults map(QueryResultConverter converter) { + + Assert.notNull(converter, "QueryResultConverter must not be null"); + + return new ReactiveFindSupport<>(template, domainType, returnType, this.resultConverter.andThen(converter), + collection, query); } @Override @@ -141,7 +155,8 @@ public Flux all() { @Override public Mono> scroll(ScrollPosition scrollPosition) { - return template.doScroll(query.with(scrollPosition), domainType, returnType, getCollectionName()); + return template.doScroll(query.with(scrollPosition), domainType, returnType, resultConverter, + getCollectionName()); } @Override @@ -151,7 +166,7 @@ public Flux tail() { @Override public TerminatingFindNear near(NearQuery nearQuery) { - return () -> template.geoNear(nearQuery, domainType, getCollectionName(), returnType); + return new TerminatingFindNearSupport<>(nearQuery, resultConverter); } @Override @@ -178,14 +193,15 @@ private Flux doFind(@Nullable FindPublisherPreparer preparer) { Document fieldsObject = query.getFieldsObject(); return template.doFind(getCollectionName(), ReactiveCollectionPreparerDelegate.of(query), queryObject, - fieldsObject, domainType, returnType, preparer != null ? preparer : getCursorPreparer(query)); + fieldsObject, domainType, returnType, resultConverter, + preparer != null ? preparer : getCursorPreparer(query)); } - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "rawtypes" }) private Flux doFindDistinct(String field) { return template.findDistinct(query, field, getCollectionName(), domainType, - returnType == domainType ? (Class) Object.class : returnType); + returnType == domainType ? (Class) Object.class : returnType); } private FindPublisherPreparer getCursorPreparer(Query query) { @@ -200,10 +216,36 @@ private String asString() { return SerializationUtils.serializeToJsonSafely(query); } + class TerminatingFindNearSupport implements TerminatingFindNear { + + private final NearQuery nearQuery; + private final QueryResultConverter resultConverter; + + public TerminatingFindNearSupport(NearQuery nearQuery, + QueryResultConverter resultConverter) { + this.nearQuery = nearQuery; + this.resultConverter = resultConverter; + } + + @Override + public TerminatingFindNear map(QueryResultConverter converter) { + + Assert.notNull(converter, "QueryResultConverter must not be null"); + + return new TerminatingFindNearSupport<>(nearQuery, this.resultConverter.andThen(converter)); + } + + @Override + public Flux> all() { + return template.doGeoNear(nearQuery, domainType, getCollectionName(), returnType, resultConverter); + } + } + /** * @author Christoph Strobl * @since 2.1 */ + @SuppressWarnings({ "unchecked", "rawtypes" }) static class DistinctOperationSupport implements TerminatingDistinct { private final String field; @@ -224,12 +266,11 @@ public TerminatingDistinct as(Class resultType) { } @Override - @SuppressWarnings("unchecked") public TerminatingDistinct matching(Query query) { Assert.notNull(query, "Query must not be null"); - return new DistinctOperationSupport<>((ReactiveFindSupport) delegate.matching(query), field); + return new DistinctOperationSupport<>((ReactiveFindSupport) delegate.matching(query), field); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveInsertOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveInsertOperationSupport.java index 06d3c6eae7..9d424c2446 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveInsertOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveInsertOperationSupport.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -50,9 +51,9 @@ static class ReactiveInsertSupport implements ReactiveInsert { private final ReactiveMongoTemplate template; private final Class domainType; - private final String collection; + private final @Nullable String collection; - ReactiveInsertSupport(ReactiveMongoTemplate template, Class domainType, String collection) { + ReactiveInsertSupport(ReactiveMongoTemplate template, Class domainType, @Nullable String collection) { this.template = template; this.domainType = domainType; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupport.java index 4f0d395950..4e3379bad0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupport.java @@ -17,9 +17,9 @@ import reactor.core.publisher.Flux; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -89,8 +89,11 @@ static class ReactiveMapReduceSupport @Override public Flux all() { + Assert.notNull(mapFunction, "MapFunction must be set first"); + Assert.notNull(reduceFunction, "ReduceFunction must be set first"); + return template.mapReduce(query, domainType, getCollectionName(), returnType, mapFunction, reduceFunction, - options); + options != null ? options : MapReduceOptions.options()); } /* diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoClientFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoClientFactoryBean.java index 89d1cd78ac..89caf3273c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoClientFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoClientFactoryBean.java @@ -16,10 +16,10 @@ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.config.AbstractFactoryBean; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import com.mongodb.MongoClientSettings; @@ -89,7 +89,7 @@ public void setExceptionTranslator(@Nullable PersistenceExceptionTranslator exce } @Override - public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + public @Nullable DataAccessException translateExceptionIfPossible(RuntimeException ex) { return exceptionTranslator.translateExceptionIfPossible(ex); } @@ -124,7 +124,9 @@ protected MongoClient createInstance() throws Exception { @Override protected void destroyInstance(@Nullable MongoClient instance) throws Exception { - instance.close(); + if (instance != null) { + instance.close(); + } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java index 90f2d2345d..14f6ee2631 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java @@ -23,6 +23,7 @@ import java.util.function.Supplier; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import org.springframework.data.domain.KeysetScrollPosition; @@ -48,7 +49,6 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -513,7 +513,7 @@ Mono> createView(String name, String source, Aggregati *

* When using {@link KeysetScrollPosition}, make sure to use non-nullable {@link org.springframework.data.domain.Sort * sort properties} as MongoDB does not support criteria to reconstruct a query result from absent document fields or - * {@code null} values through {@code $gt/$lt} operators. + * {@literal null} values through {@code $gt/$lt} operators. * * @param query the query class that specifies the criteria used to find a document and also an optional fields * specification. Must not be {@literal null}. @@ -538,7 +538,7 @@ Mono> createView(String name, String source, Aggregati *

* When using {@link KeysetScrollPosition}, make sure to use non-nullable {@link org.springframework.data.domain.Sort * sort properties} as MongoDB does not support criteria to reconstruct a query result from absent document fields or - * {@code null} values through {@code $gt/$lt} operators. + * {@literal null} values through {@code $gt/$lt} operators. * * @param query the query class that specifies the criteria used to find a document and also an optional fields * specification. Must not be {@literal null}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index ea427a3e1f..0ad473b8b7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -44,9 +44,9 @@ import org.bson.Document; import org.bson.conversions.Bson; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; - import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -113,15 +113,15 @@ import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Meta; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter; -import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; import org.springframework.data.projection.EntityProjection; import org.springframework.data.util.Optionals; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -345,7 +345,8 @@ public void setWriteConcern(@Nullable WriteConcern writeConcern) { * @param writeConcernResolver can be {@literal null}. */ public void setWriteConcernResolver(@Nullable WriteConcernResolver writeConcernResolver) { - this.writeConcernResolver = writeConcernResolver; + this.writeConcernResolver = writeConcernResolver != null ? writeConcernResolver + : DefaultWriteConcernResolver.INSTANCE; } /** @@ -739,10 +740,11 @@ public Mono collectionExists(Class entityClass) { @Override public Mono collectionExists(String collectionName) { - return createMono(db -> Flux.from(MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(db).listCollectionNames()) // - .filter(s -> s.equals(collectionName)) // - .map(s -> true) // - .single(false)); + return createMono( + db -> Flux.from(db.listCollectionNames()) // + .filter(s -> s.equals(collectionName)) // + .map(s -> true) // + .single(false)); } @Override @@ -787,7 +789,7 @@ public ReactiveBulkOperations bulkOps(BulkMode mode, @Nullable Class entityTy @Override public Flux getCollectionNames() { - return createFlux(db -> MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(db).listCollectionNames()); + return createFlux(db -> db.listCollectionNames()); } public Mono getMongoDatabase() { @@ -877,10 +879,11 @@ public Mono> scroll(Query query, Class entityType) { @Override public Mono> scroll(Query query, Class entityType, String collectionName) { - return doScroll(query, entityType, entityType, collectionName); + return doScroll(query, entityType, entityType, QueryResultConverter.entity(), collectionName); } - Mono> doScroll(Query query, Class sourceClass, Class targetClass, String collectionName) { + Mono> doScroll(Query query, Class sourceClass, Class targetClass, + QueryResultConverter resultConverter, String collectionName) { Assert.notNull(query, "Query must not be null"); Assert.notNull(collectionName, "CollectionName must not be null"); @@ -888,7 +891,7 @@ Mono> doScroll(Query query, Class sourceClass, Class targetC Assert.notNull(targetClass, "Target type must not be null"); EntityProjection projection = operations.introspectProjection(targetClass, sourceClass); - ProjectingReadCallback callback = new ProjectingReadCallback<>(mongoConverter, projection, collectionName); + DocumentCallback callback = getResultReader(projection, collectionName, resultConverter); int limit = query.isLimited() ? query.getLimit() + 1 : Integer.MAX_VALUE; if (query.hasKeyset()) { @@ -896,18 +899,18 @@ Mono> doScroll(Query query, Class sourceClass, Class targetC KeysetScrollQuery keysetPaginationQuery = ScrollUtils.createKeysetPaginationQuery(query, operations.getIdPropertyName(sourceClass)); - Mono> result = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query), + Mono> result = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query), keysetPaginationQuery.query(), keysetPaginationQuery.fields(), sourceClass, new QueryFindPublisherPreparer(query, keysetPaginationQuery.sort(), limit, 0, sourceClass), callback) - .collectList(); + .collectList(); return result.map(it -> ScrollUtils.createWindow(query, it, sourceClass, operations)); } - Mono> result = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(), + Mono> result = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(), query.getFieldsObject(), sourceClass, new QueryFindPublisherPreparer(query, query.getSortObject(), limit, query.getSkip(), sourceClass), callback) - .collectList(); + .collectList(); return result.map( it -> ScrollUtils.createWindow(it, query.getLimit(), OffsetScrollPosition.positionFunction(query.getSkip()))); @@ -1003,6 +1006,11 @@ public Flux aggregate(Aggregation aggregation, String collectionName, Cla protected Flux doAggregate(Aggregation aggregation, String collectionName, @Nullable Class inputType, Class outputType) { + return doAggregate(aggregation, collectionName, inputType, outputType, QueryResultConverter.entity()); + } + + Flux doAggregate(Aggregation aggregation, String collectionName, @Nullable Class inputType, + Class outputType, QueryResultConverter resultConverter) { Assert.notNull(aggregation, "Aggregation pipeline must not be null"); Assert.hasText(collectionName, "Collection name must not be null or empty"); @@ -1018,13 +1026,14 @@ protected Flux doAggregate(Aggregation aggregation, String collectionName serializeToJsonSafely(ctx.getAggregationPipeline()), collectionName)); } - ReadDocumentCallback readCallback = new ReadDocumentCallback<>(mongoConverter, outputType, collectionName); + DocumentCallback readCallback = new QueryResultConverterCallback<>(resultConverter, + new ReadDocumentCallback<>(mongoConverter, outputType, collectionName)); return execute(collectionName, collection -> aggregateAndMap(collection, ctx.getAggregationPipeline(), ctx.isOutOrMerge(), options, readCallback, ctx.getInputType())); } private Flux aggregateAndMap(MongoCollection collection, List pipeline, - boolean isOutOrMerge, AggregationOptions options, ReadDocumentCallback readCallback, + boolean isOutOrMerge, AggregationOptions options, DocumentCallback readCallback, @Nullable Class inputType) { ReactiveCollectionPreparerDelegate collectionPreparer = ReactiveCollectionPreparerDelegate.of(options); @@ -1070,9 +1079,14 @@ public Flux> geoNear(NearQuery near, Class entityClass, Stri return geoNear(near, entityClass, collectionName, entityClass); } - @SuppressWarnings("unchecked") protected Flux> geoNear(NearQuery near, Class entityClass, String collectionName, Class returnType) { + return doGeoNear(near, entityClass, collectionName, returnType, QueryResultConverter.entity()); + } + + @SuppressWarnings("unchecked") + Flux> doGeoNear(NearQuery near, Class entityClass, String collectionName, Class returnType, + QueryResultConverter resultConverter) { if (near == null) { throw new InvalidDataAccessApiUsageException("NearQuery must not be null"); @@ -1086,8 +1100,8 @@ protected Flux> geoNear(NearQuery near, Class entityClass, S String distanceField = operations.nearQueryDistanceFieldName(entityClass); EntityProjection projection = operations.introspectProjection(returnType, entityClass); - GeoNearResultDocumentCallback callback = new GeoNearResultDocumentCallback<>(distanceField, - new ProjectingReadCallback<>(mongoConverter, projection, collection), near.getMetric()); + GeoNearResultDocumentCallback callback = new GeoNearResultDocumentCallback<>(distanceField, + getResultReader(projection, collectionName, resultConverter), near.getMetric()); Builder optionsBuilder = AggregationOptions.builder(); if (near.hasReadPreference()) { @@ -1123,9 +1137,8 @@ public Mono findAndModify(Query query, UpdateDefinition update, FindAndMo return findAndModify(query, update, options, entityClass, getCollectionName(entityClass)); } - @Override - public Mono findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, - Class entityClass, String collectionName) { + public Mono findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, + Class entityClass, String collectionName, QueryResultConverter resultConverter) { Assert.notNull(options, "Options must not be null "); Assert.notNull(entityClass, "Entity class must not be null"); @@ -1142,12 +1155,27 @@ public Mono findAndModify(Query query, UpdateDefinition update, FindAndMo } return doFindAndModify(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(), - query.getFieldsObject(), getMappedSortObject(query, entityClass), entityClass, update, optionsToUse); + query.getFieldsObject(), getMappedSortObject(query, entityClass), entityClass, update, optionsToUse, + resultConverter); + } + + @Override + public Mono findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, + Class entityClass, String collectionName) { + return findAndModify(query, update, options, entityClass, collectionName, QueryResultConverter.entity()); } @Override public Mono findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class entityType, String collectionName, Class resultType) { + return findAndReplace(query, replacement, options, entityType, collectionName, resultType, + QueryResultConverter.entity()); + } + + @SuppressWarnings("NullAway") + public Mono findAndReplace(Query query, S replacement, FindAndReplaceOptions options, + Class entityType, String collectionName, Class resultType, + QueryResultConverter resultConverter) { Assert.notNull(query, "Query must not be null"); Assert.notNull(replacement, "Replacement must not be null"); @@ -1184,9 +1212,9 @@ public Mono findAndReplace(Query query, S replacement, FindAndReplaceO mapped.getCollection())); }).flatMap(it -> { - Mono afterFindAndReplace = doFindAndReplace(it.getCollection(), collectionPreparer, mappedQuery, + Mono afterFindAndReplace = doFindAndReplace(it.getCollection(), collectionPreparer, mappedQuery, mappedFields, mappedSort, queryContext.getCollation(entityType).orElse(null), entityType, it.getTarget(), - options, projection); + options, projection, resultConverter); return afterFindAndReplace.flatMap(saved -> { maybeEmitEvent(new AfterSaveEvent<>(saved, it.getTarget(), it.getCollection())); return maybeCallAfterSave(saved, it.getTarget(), it.getCollection()); @@ -1351,6 +1379,7 @@ public Mono insert(T objectToSave, String collectionName) { return doInsert(collectionName, objectToSave, this.mongoConverter); } + @SuppressWarnings("NullAway") protected Mono doInsert(String collectionName, T objectToSave, MongoWriter writer) { return Mono.just(PersistableEntityModel.of(objectToSave, collectionName)) // @@ -1401,6 +1430,7 @@ public Flux insertAll(Mono> objectsToSa return Flux.from(objectsToSave).flatMapSequential(this::insertAll); } + @SuppressWarnings("NullAway") protected Flux doInsertAll(Collection listToSave, MongoWriter writer) { Map> elementsByCollection = new HashMap<>(); @@ -1417,6 +1447,7 @@ protected Flux doInsertAll(Collection listToSave, MongoWrite .concatMap(collectionName -> doInsertBatch(collectionName, elementsByCollection.get(collectionName), writer)); } + @SuppressWarnings("NullAway") protected Flux doInsertBatch(String collectionName, Collection batchToSave, MongoWriter writer) { @@ -1434,11 +1465,16 @@ protected Flux doInsertBatch(String collectionName, Collection(initialized, dbDoc, collectionName)); + maybeEmitEvent(new BeforeSaveEvent<>(initialized, mapped.getDocument(), collectionName)); + return maybeCallBeforeSave(initialized, mapped.getDocument(), collectionName).map(toSave -> { - return maybeCallBeforeSave(initialized, dbDoc, collectionName).thenReturn(Tuples.of(entity, dbDoc)); + MappedDocument mappedDocument = queryOperations.createInsertContext(mapped) + .prepareId(uninitialized.getClass()); + + return Tuples.of(entity, mappedDocument.getDocument()); + }); }); }).collectList(); @@ -1532,6 +1568,7 @@ private Mono doSaveVersioned(AdaptibleEntity source, String collection }); } + @SuppressWarnings("NullAway") protected Mono doSave(String collectionName, T objectToSave, MongoWriter writer) { assertUpdateableIdIfNotSet(objectToSave); @@ -1625,6 +1662,7 @@ private MongoCollection prepareCollection(MongoCollection co return collectionToUse; } + @SuppressWarnings("NullAway") protected Mono saveDocument(String collectionName, Document document, Class entityClass) { if (LOGGER.isDebugEnabled()) { @@ -1728,7 +1766,8 @@ public Mono updateMulti(Query query, UpdateDefinition update, Clas return doUpdate(collectionName, query, update, entityClass, false, true); } - protected Mono doUpdate(String collectionName, Query query, @Nullable UpdateDefinition update, + @SuppressWarnings("NullAway") + protected Mono doUpdate(String collectionName, Query query, UpdateDefinition update, @Nullable Class entityClass, boolean upsert, boolean multi) { MongoPersistentEntity entity = entityClass == null ? null : getPersistentEntity(entityClass); @@ -1810,7 +1849,8 @@ protected Mono doUpdate(String collectionName, Query query, @Nulla Document updateObj = updateContext.getMappedUpdate(entity); if (containsVersionProperty(queryObj, entity)) - throw new OptimisticLockingFailureException("Optimistic lock exception on saving entity %s to collection %s".formatted(entity.getName(), collectionName)); + throw new OptimisticLockingFailureException("Optimistic lock exception on saving entity %s to collection %s" + .formatted(entity.getName(), collectionName)); } } }); @@ -2008,18 +2048,18 @@ public Flux tail(Query query, Class entityClass) { @Override public Flux tail(@Nullable Query query, Class entityClass, String collectionName) { - ReactiveCollectionPreparerDelegate collectionPreparer = ReactiveCollectionPreparerDelegate.of(query); if (query == null) { LOGGER.debug(String.format("Tail for class: %s in collection: %s", entityClass, collectionName)); return executeFindMultiInternal( - collection -> new FindCallback(collectionPreparer, null).doInCollection(collection) + collection -> new FindCallback(CollectionPreparer.identity(), null).doInCollection(collection) .cursorType(CursorType.TailableAwait), FindPublisherPreparer.NO_OP_PREPARER, new ReadDocumentCallback<>(mongoConverter, entityClass, collectionName), collectionName); } + ReactiveCollectionPreparerDelegate collectionPreparer = ReactiveCollectionPreparerDelegate.of(query); return doFind(collectionName, collectionPreparer, query.getQueryObject(), query.getFieldsObject(), entityClass, new TailingQueryFindPublisherPreparer(query, entityClass)); } @@ -2171,10 +2211,6 @@ public Flux mapReduce(Query filterQuery, Class domainType, String inpu publisher = publisher.jsMode(options.getJavaScriptMode()); } - if (options.getOutputSharded().isPresent()) { - MongoCompatibilityAdapter.mapReducePublisherAdapter(publisher).sharded(options.getOutputSharded().get()); - } - if (StringUtils.hasText(options.getOutputCollection()) && !options.usesInlineOutput()) { publisher = publisher.collectionName(options.getOutputCollection()).action(options.getMapReduceAction()); @@ -2257,6 +2293,44 @@ protected Flux doFindAndDelete(String collectionName, Query query, Class< .flatMapSequential(deleteResult -> Flux.fromIterable(list))); } + @SuppressWarnings({"rawtypes", "unchecked", "NullAway"}) + Flux doFindAndDelete(String collectionName, Query query, Class entityClass, + QueryResultConverter resultConverter) { + + List ids = new ArrayList<>(); + ProjectingReadCallback readCallback = new ProjectingReadCallback(getConverter(), + EntityProjection.nonProjecting(entityClass), collectionName); + + QueryResultConverterCallback callback = new QueryResultConverterCallback<>(resultConverter, readCallback) { + + @Override + public Mono doWith(Document object) { + ids.add(object.get("_id")); + return super.doWith(object); + } + }; + + Flux flux = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(), + query.getFieldsObject(), entityClass, + new QueryFindPublisherPreparer(query, query.getSortObject(), query.getLimit(), query.getSkip(), entityClass), + callback); + + return Flux.from(flux).collectList().filter(it -> !it.isEmpty()).flatMapMany(list -> { + + Criteria[] criterias = ids.stream() // + .map(it -> Criteria.where("_id").is(it)) // + .toArray(Criteria[]::new); + + Query removeQuery = new Query(criterias.length == 1 ? criterias[0] : new Criteria().orOperator(criterias)); + if (query.hasReadPreference()) { + removeQuery.withReadPreference(query.getReadPreference()); + } + + return Flux.from(remove(removeQuery, entityClass, collectionName)) + .flatMapSequential(deleteResult -> Flux.fromIterable(list)); + }); + } + /** * Create the specified collection using the provided options * @@ -2382,8 +2456,8 @@ protected Flux doFind(String collectionName, serializeToJsonSafely(mappedQuery), mappedFields, entityClass, collectionName)); } - return executeFindMultiInternal(new FindCallback(collectionPreparer, mappedQuery, mappedFields), preparer, - objectCallback, collectionName); + return executeFindMultiInternal(new FindCallback(collectionPreparer, mappedQuery, mappedFields), + preparer != null ? preparer : FindPublisherPreparer.NO_OP_PREPARER, objectCallback, collectionName); } CollectionPreparer> createCollectionPreparer(Query query) { @@ -2407,11 +2481,12 @@ CollectionPreparer> createCollectionPreparer(Query que * * @since 2.0 */ - Flux doFind(String collectionName, CollectionPreparer> collectionPreparer, - Document query, Document fields, Class sourceClass, Class targetClass, FindPublisherPreparer preparer) { + Flux doFind(String collectionName, CollectionPreparer> collectionPreparer, + Document query, Document fields, Class sourceClass, Class targetClass, + QueryResultConverter resultConverter, FindPublisherPreparer preparer) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(sourceClass); - EntityProjection projection = operations.introspectProjection(targetClass, sourceClass); + EntityProjection projection = operations.introspectProjection(targetClass, sourceClass); QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields)); Document mappedFields = queryContext.getMappedFields(entity, projection); @@ -2423,7 +2498,7 @@ Flux doFind(String collectionName, CollectionPreparer(mongoConverter, projection, collectionName), collectionName); + getResultReader(projection, collectionName, resultConverter), collectionName); } protected CreateCollectionOptions convertToCreateCollectionOptions(@Nullable CollectionOptions collectionOptions) { @@ -2448,8 +2523,8 @@ protected CreateCollectionOptions convertToCreateCollectionOptions(@Nullable Col * @return the List of converted objects. */ protected Mono doFindAndRemove(String collectionName, - CollectionPreparer> collectionPreparer, Document query, Document fields, Document sort, - @Nullable Collation collation, Class entityClass) { + CollectionPreparer> collectionPreparer, Document query, Document fields, + @Nullable Document sort, @Nullable Collation collation, Class entityClass) { if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("findAndRemove using query: %s fields: %s sort: %s for class: %s in collection: %s", @@ -2463,9 +2538,10 @@ protected Mono doFindAndRemove(String collectionName, new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName); } - protected Mono doFindAndModify(String collectionName, - CollectionPreparer> collectionPreparer, Document query, Document fields, Document sort, - Class entityClass, UpdateDefinition update, FindAndModifyOptions options) { + Mono doFindAndModify(String collectionName, + CollectionPreparer> collectionPreparer, Document query, Document fields, + @Nullable Document sort, Class entityClass, UpdateDefinition update, FindAndModifyOptions options, + QueryResultConverter resultConverter) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); UpdateContext updateContext = queryOperations.updateSingleContext(update, query, false); @@ -2481,14 +2557,16 @@ protected Mono doFindAndModify(String collectionName, LOGGER.debug(String.format( "findAndModify using query: %s fields: %s sort: %s for class: %s and update: %s " + "in collection: %s", serializeToJsonSafely(mappedQuery), fields, serializeToJsonSafely(sort), entityClass, - serializeToJsonSafely(mappedUpdate), - collectionName)); + serializeToJsonSafely(mappedUpdate), collectionName)); } + EntityProjection projection = EntityProjection.nonProjecting(entityClass); + DocumentCallback callback = getResultReader(projection, collectionName, resultConverter); + return executeFindOneInternal( new FindAndModifyCallback(collectionPreparer, mappedQuery, fields, sort, mappedUpdate, update.getArrayFilters().stream().map(ArrayFilter::asDocument).collect(Collectors.toList()), options), - new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName); + callback, collectionName); }); } @@ -2517,7 +2595,7 @@ protected Mono doFindAndReplace(String collectionName, EntityProjection projection = operations.introspectProjection(resultType, entityType); return doFindAndReplace(collectionName, collectionPreparer, mappedQuery, mappedFields, mappedSort, collation, - entityType, replacement, options, projection); + entityType, replacement, options, projection, QueryResultConverter.entity()); } /** @@ -2537,10 +2615,11 @@ protected Mono doFindAndReplace(String collectionName, * {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}. * @since 3.4 */ - private Mono doFindAndReplace(String collectionName, + private Mono doFindAndReplace(String collectionName, CollectionPreparer> collectionPreparer, Document mappedQuery, Document mappedFields, Document mappedSort, com.mongodb.client.model.Collation collation, Class entityType, Document replacement, - FindAndReplaceOptions options, EntityProjection projection) { + FindAndReplaceOptions options, EntityProjection projection, + QueryResultConverter resultConverter) { return Mono.defer(() -> { @@ -2552,9 +2631,10 @@ private Mono doFindAndReplace(String collectionName, serializeToJsonSafely(replacement), collectionName)); } + DocumentCallback resultReader = getResultReader(projection, collectionName, resultConverter); + return executeFindOneInternal(new FindAndReplaceCallback(collectionPreparer, mappedQuery, mappedFields, - mappedSort, replacement, collation, options), - new ProjectingReadCallback<>(this.mongoConverter, projection, collectionName), collectionName); + mappedSort, replacement, collation, options), resultReader, collectionName); }); } @@ -2659,8 +2739,7 @@ protected MongoDatabase prepareDatabase(MongoDatabase database) { * @see #setWriteConcern(WriteConcern) * @see #setWriteConcernResolver(WriteConcernResolver) */ - @Nullable - protected WriteConcern prepareWriteConcern(MongoAction mongoAction) { + protected @Nullable WriteConcern prepareWriteConcern(MongoAction mongoAction) { WriteConcern wc = writeConcernResolver.resolve(mongoAction); return potentiallyForceAcknowledgedWrite(wc); @@ -2679,7 +2758,7 @@ private WriteConcern potentiallyForceAcknowledgedWrite(@Nullable WriteConcern wc if (ObjectUtils.nullSafeEquals(WriteResultChecking.EXCEPTION, writeResultChecking)) { if (wc == null || wc.getWObject() == null - || (wc.getWObject()instanceof Number concern && concern.intValue() < 1)) { + || (wc.getWObject() instanceof Number concern && concern.intValue() < 1)) { return WriteConcern.ACKNOWLEDGED; } } @@ -2725,7 +2804,7 @@ private Mono executeFindOneInternal(ReactiveCollectionCallback * @return */ private Flux executeFindMultiInternal(ReactiveCollectionQueryCallback collectionCallback, - @Nullable FindPublisherPreparer preparer, DocumentCallback objectCallback, String collectionName) { + FindPublisherPreparer preparer, DocumentCallback objectCallback, String collectionName) { return createFlux(collectionName, collection -> { return Flux.from(preparer.initiateFind(collection, collectionCallback::doInCollection)) @@ -2733,6 +2812,16 @@ private Flux executeFindMultiInternal(ReactiveCollectionQueryCallback DocumentCallback getResultReader(EntityProjection projection, String collectionName, + QueryResultConverter resultConverter) { + + DocumentCallback readCallback = new ProjectingReadCallback<>(mongoConverter, projection, collectionName); + + return resultConverter == QueryResultConverter.entity() ? (DocumentCallback) readCallback + : new QueryResultConverterCallback<>(resultConverter, readCallback); + } + /** * Exception translation {@link Function} intended for {@link Flux#onErrorMap(Function)} usage. * @@ -2764,8 +2853,7 @@ private static RuntimeException potentiallyConvertRuntimeException(RuntimeExcept return resolved == null ? ex : resolved; } - @Nullable - private MongoPersistentEntity getPersistentEntity(@Nullable Class type) { + private @Nullable MongoPersistentEntity getPersistentEntity(@Nullable Class type) { return type == null ? null : mappingContext.getPersistentEntity(type); } @@ -2785,8 +2873,8 @@ private MappingMongoConverter getDefaultMongoConverter() { return converter; } - @Nullable - private Document getMappedSortObject(Query query, Class type) { + @Contract("null, _ -> null") + private @Nullable Document getMappedSortObject(@Nullable Query query, Class type) { if (query == null) { return null; @@ -2795,8 +2883,8 @@ private Document getMappedSortObject(Query query, Class type) { return getMappedSortObject(query.getSortObject(), type); } - @Nullable - private Document getMappedSortObject(Document sortObject, Class type) { + @Contract("null, _ -> null") + private @Nullable Document getMappedSortObject(@Nullable Document sortObject, Class type) { if (ObjectUtils.isEmpty(sortObject)) { return null; @@ -2862,7 +2950,8 @@ private static class FindCallback implements ReactiveCollectionQueryCallback> collectionPreparer, Document query, Document fields) { + FindCallback(CollectionPreparer> collectionPreparer, @Nullable Document query, + @Nullable Document fields) { this.collectionPreparer = collectionPreparer; this.query = query; this.fields = fields; @@ -2898,11 +2987,11 @@ private static class FindAndRemoveCallback implements ReactiveCollectionCallback private final CollectionPreparer> collectionPreparer; private final Document query; private final Document fields; - private final Document sort; + private final @Nullable Document sort; private final Optional collation; FindAndRemoveCallback(CollectionPreparer> collectionPreparer, Document query, - Document fields, Document sort, @Nullable Collation collation) { + Document fields, @Nullable Document sort, @Nullable Collation collation) { this.collectionPreparer = collectionPreparer; this.query = query; this.fields = fields; @@ -2928,14 +3017,15 @@ private static class FindAndModifyCallback implements ReactiveCollectionCallback private final CollectionPreparer> collectionPreparer; private final Document query; - private final Document fields; - private final Document sort; + private final @Nullable Document fields; + private final @Nullable Document sort; private final Object update; private final List arrayFilters; private final FindAndModifyOptions options; FindAndModifyCallback(CollectionPreparer> collectionPreparer, Document query, - Document fields, Document sort, Object update, List arrayFilters, FindAndModifyOptions options) { + @Nullable Document fields, @Nullable Document sort, Object update, List arrayFilters, + FindAndModifyOptions options) { this.collectionPreparer = collectionPreparer; this.query = query; @@ -2973,7 +3063,7 @@ public Publisher doInCollection(MongoCollection collection) } private static FindOneAndUpdateOptions convertToFindOneAndUpdateOptions(FindAndModifyOptions options, - Document fields, Document sort, List arrayFilters) { + @Nullable Document fields, @Nullable Document sort, List arrayFilters) { FindOneAndUpdateOptions result = new FindOneAndUpdateOptions(); @@ -3009,11 +3099,11 @@ private static class FindAndReplaceCallback implements ReactiveCollectionCallbac private final Document fields; private final Document sort; private final Document update; - private final @Nullable com.mongodb.client.model.Collation collation; + private final com.mongodb.client.model.@Nullable Collation collation; private final FindAndReplaceOptions options; FindAndReplaceCallback(CollectionPreparer> collectionPreparer, Document query, - Document fields, Document sort, Document update, com.mongodb.client.model.Collation collation, + Document fields, Document sort, Document update, com.mongodb.client.model.@Nullable Collation collation, FindAndReplaceOptions options) { this.collectionPreparer = collectionPreparer; this.query = query; @@ -3049,7 +3139,8 @@ private FindOneAndReplaceOptions convertToFindOneAndReplaceOptions(FindAndReplac } } - private static FindOneAndDeleteOptions convertToFindOneAndDeleteOptions(Document fields, Document sort) { + private static FindOneAndDeleteOptions convertToFindOneAndDeleteOptions(@Nullable Document fields, + @Nullable Document sort) { FindOneAndDeleteOptions result = new FindOneAndDeleteOptions(); result = result.projection(fields).sort(sort); @@ -3090,6 +3181,22 @@ interface ReactiveCollectionQueryCallback extends ReactiveCollectionCallback< FindPublisher doInCollection(MongoCollection collection) throws MongoException, DataAccessException; } + static class QueryResultConverterCallback implements DocumentCallback { + + private final QueryResultConverter converter; + private final DocumentCallback delegate; + + QueryResultConverterCallback(QueryResultConverter converter, DocumentCallback delegate) { + this.converter = converter; + this.delegate = delegate; + } + + @Override + public Mono doWith(Document object) { + return delegate.doWith(object).map(it -> converter.mapDocument(object, () -> it)); + } + } + /** * Simple {@link DocumentCallback} that will transform {@link Document} into the given target type using the given * {@link EntityReader}. @@ -3206,7 +3313,7 @@ public Mono> doWith(Document object) { double distance = getDistance(object); - return delegate.doWith(object).map(doWith -> new GeoResult<>(doWith, new Distance(distance, metric))); + return delegate.doWith(object).map(doWith -> new GeoResult<>(doWith, Distance.of(distance, metric))); } double getDistance(Document object) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperation.java index 378f13d917..dd515cb37c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperation.java @@ -20,6 +20,7 @@ import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.lang.Contract; import com.mongodb.client.result.DeleteResult; @@ -56,16 +57,22 @@ public interface ReactiveRemoveOperation { ReactiveRemove remove(Class domainType); /** - * Compose remove execution by calling one of the terminating methods. + * @author Christoph Strobl + * @since 5.0 */ - interface TerminatingRemove { + interface TerminatingResults { /** - * Remove all documents matching. + * Map the query result to a different type using {@link QueryResultConverter}. * - * @return {@link Mono} emitting the {@link DeleteResult}. Never {@literal null}. + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link ExecutableFindOperation.TerminatingResults}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since 5.0 */ - Mono all(); + @Contract("_ -> new") + TerminatingResults map(QueryResultConverter converter); /** * Remove and return all matching documents.
@@ -78,6 +85,20 @@ interface TerminatingRemove { Flux findAndRemove(); } + /** + * Compose remove execution by calling one of the terminating methods. + */ + interface TerminatingRemove extends TerminatingResults { + + /** + * Remove all documents matching. + * + * @return {@link Mono} emitting the {@link DeleteResult}. Never {@literal null}. + */ + Mono all(); + + } + /** * Collection override (optional). */ diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupport.java index 97c9cb0d0e..f77b5296d7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupport.java @@ -18,6 +18,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Query; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -46,22 +47,25 @@ public ReactiveRemove remove(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); - return new ReactiveRemoveSupport<>(template, domainType, ALL_QUERY, null); + return new ReactiveRemoveSupport<>(template, domainType, ALL_QUERY, null, QueryResultConverter.entity()); } - static class ReactiveRemoveSupport implements ReactiveRemove, RemoveWithCollection { + static class ReactiveRemoveSupport implements ReactiveRemove, RemoveWithCollection { private final ReactiveMongoTemplate template; - private final Class domainType; + private final Class domainType; private final Query query; - private final String collection; + private final @Nullable String collection; + private final QueryResultConverter resultConverter; - ReactiveRemoveSupport(ReactiveMongoTemplate template, Class domainType, Query query, String collection) { + ReactiveRemoveSupport(ReactiveMongoTemplate template, Class domainType, Query query, @Nullable String collection, + QueryResultConverter resultConverter) { this.template = template; this.domainType = domainType; this.query = query; this.collection = collection; + this.resultConverter = resultConverter; } @Override @@ -69,7 +73,7 @@ public RemoveWithQuery inCollection(String collection) { Assert.hasText(collection, "Collection must not be null nor empty"); - return new ReactiveRemoveSupport<>(template, domainType, query, collection); + return new ReactiveRemoveSupport<>(template, domainType, query, collection, resultConverter); } @Override @@ -77,7 +81,7 @@ public TerminatingRemove matching(Query query) { Assert.notNull(query, "Query must not be null"); - return new ReactiveRemoveSupport<>(template, domainType, query, collection); + return new ReactiveRemoveSupport<>(template, domainType, query, collection, resultConverter); } @Override @@ -93,7 +97,13 @@ public Flux findAndRemove() { String collectionName = getCollectionName(); - return template.doFindAndDelete(collectionName, query, domainType); + return template.doFindAndDelete(collectionName, query, domainType, resultConverter); + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public TerminatingResults map(QueryResultConverter converter) { + return new ReactiveRemoveSupport<>(template, (Class) domainType, query, collection, converter); } private String getCollectionName() { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java index 51f75f3265..c9f92029cc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core; +import org.jetbrains.annotations.Contract; import reactor.core.publisher.Mono; import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; @@ -64,6 +65,18 @@ public interface ReactiveUpdateOperation { */ interface TerminatingFindAndModify { + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link TerminatingFindAndModify}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since 5.0 + */ + @Contract("_ -> new") + TerminatingFindAndModify map(QueryResultConverter converter); + /** * Find, modify and return the first matching document. * @@ -97,6 +110,18 @@ interface TerminatingReplace { */ interface TerminatingFindAndReplace extends TerminatingReplace { + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link TerminatingFindAndModify}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since 5.0 + */ + @Contract("_ -> new") + TerminatingFindAndReplace map(QueryResultConverter converter); + /** * Find, replace and return the first matching document. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java index 51cd99dc93..876a7a5aa2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java @@ -17,9 +17,9 @@ import reactor.core.publisher.Mono; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -47,26 +47,27 @@ public ReactiveUpdate update(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); - return new ReactiveUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null, domainType); + return new ReactiveUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null, domainType, QueryResultConverter.entity()); } - static class ReactiveUpdateSupport + static class ReactiveUpdateSupport implements ReactiveUpdate, UpdateWithCollection, UpdateWithQuery, TerminatingUpdate, FindAndReplaceWithOptions, FindAndReplaceWithProjection, TerminatingFindAndReplace { private final ReactiveMongoTemplate template; private final Class domainType; private final Query query; - private final org.springframework.data.mongodb.core.query.UpdateDefinition update; - @Nullable private final String collection; - @Nullable private final FindAndModifyOptions findAndModifyOptions; - @Nullable private final FindAndReplaceOptions findAndReplaceOptions; - @Nullable private final Object replacement; - private final Class targetType; - - ReactiveUpdateSupport(ReactiveMongoTemplate template, Class domainType, Query query, UpdateDefinition update, - String collection, FindAndModifyOptions findAndModifyOptions, FindAndReplaceOptions findAndReplaceOptions, - Object replacement, Class targetType) { + private final org.springframework.data.mongodb.core.query.@Nullable UpdateDefinition update; + private final @Nullable String collection; + private final @Nullable FindAndModifyOptions findAndModifyOptions; + private final @Nullable FindAndReplaceOptions findAndReplaceOptions; + private final @Nullable Object replacement; + private final Class targetType; + private final QueryResultConverter resultConverter; + + ReactiveUpdateSupport(ReactiveMongoTemplate template, Class domainType, Query query, @Nullable UpdateDefinition update, + @Nullable String collection, @Nullable FindAndModifyOptions findAndModifyOptions, @Nullable FindAndReplaceOptions findAndReplaceOptions, + @Nullable Object replacement, Class targetType, QueryResultConverter resultConverter) { this.template = template; this.domainType = domainType; @@ -77,6 +78,7 @@ static class ReactiveUpdateSupport this.findAndReplaceOptions = findAndReplaceOptions; this.replacement = replacement; this.targetType = targetType; + this.resultConverter = resultConverter; } @Override @@ -85,7 +87,7 @@ public TerminatingUpdate apply(org.springframework.data.mongodb.core.query.Up Assert.notNull(update, "Update must not be null"); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override @@ -94,7 +96,7 @@ public UpdateWithQuery inCollection(String collection) { Assert.hasText(collection, "Collection must not be null nor empty"); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override @@ -108,20 +110,25 @@ public Mono upsert() { } @Override + @SuppressWarnings({"unchecked", "rawtypes", "NullAway"}) public Mono findAndModify() { String collectionName = getCollectionName(); return template.findAndModify(query, update, - findAndModifyOptions != null ? findAndModifyOptions : FindAndModifyOptions.none(), targetType, - collectionName); + findAndModifyOptions != null ? findAndModifyOptions : FindAndModifyOptions.none(), (Class) targetType, + collectionName, resultConverter); } @Override + @SuppressWarnings({"unchecked","rawtypes"}) public Mono findAndReplace() { + + Assert.notNull(replacement, "Replacement must be set first"); + return template.findAndReplace(query, replacement, findAndReplaceOptions != null ? findAndReplaceOptions : FindAndReplaceOptions.none(), (Class) domainType, - getCollectionName(), targetType); + getCollectionName(), targetType, resultConverter); } @Override @@ -130,7 +137,7 @@ public UpdateWithUpdate matching(Query query) { Assert.notNull(query, "Query must not be null"); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override @@ -144,7 +151,7 @@ public TerminatingFindAndModify withOptions(FindAndModifyOptions options) { Assert.notNull(options, "Options must not be null"); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, options, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override @@ -153,7 +160,7 @@ public FindAndReplaceWithProjection replaceWith(T replacement) { Assert.notNull(replacement, "Replacement must not be null"); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override @@ -162,7 +169,7 @@ public FindAndReplaceWithProjection withOptions(FindAndReplaceOptions options Assert.notNull(options, "Options must not be null"); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, options, - replacement, targetType); + replacement, targetType, resultConverter); } @Override @@ -173,7 +180,7 @@ public TerminatingReplace withOptions(ReplaceOptions options) { target.upsert(); } return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - target, replacement, targetType); + target, replacement, targetType, resultConverter); } @Override @@ -182,10 +189,17 @@ public FindAndReplaceWithOptions as(Class resultType) { Assert.notNull(resultType, "ResultType must not be null"); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, resultType); + findAndReplaceOptions, replacement, resultType, QueryResultConverter.entity()); + } + + @Override + public ReactiveUpdateSupport map(QueryResultConverter converter) { + return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, + findAndReplaceOptions, replacement, targetType, this.resultConverter.andThen(converter)); } @Override + @SuppressWarnings("NullAway") public Mono replaceFirst() { if (replacement != null) { @@ -197,6 +211,7 @@ public Mono replaceFirst() { findAndReplaceOptions != null ? findAndReplaceOptions : ReplaceOptions.none(), getCollectionName()); } + @SuppressWarnings("NullAway") private Mono doUpdate(boolean multi, boolean upsert) { return template.doUpdate(getCollectionName(), query, update, domainType, upsert, multi); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadConcernAware.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadConcernAware.java index 00c5815fc9..7a7e5fdfb2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadConcernAware.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadConcernAware.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import com.mongodb.ReadConcern; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadPreferenceAware.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadPreferenceAware.java index 74bca9abea..e6f3fc0daf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadPreferenceAware.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadPreferenceAware.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import com.mongodb.ReadPreference; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReplaceOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReplaceOptions.java index a2e2ba24c0..a487cde669 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReplaceOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReplaceOptions.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.lang.Contract; /** * Options for {@link org.springframework.data.mongodb.core.MongoOperations#replace(Query, Object) replace operations}. Defaults to @@ -69,6 +70,7 @@ public static ReplaceOptions none() { * * @return this. */ + @Contract("-> this") public ReplaceOptions upsert() { this.upsert = true; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScriptOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScriptOperations.java index a01760368a..2ec71b415a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScriptOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScriptOperations.java @@ -17,9 +17,9 @@ import java.util.Set; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.script.ExecutableMongoScript; import org.springframework.data.mongodb.core.script.NamedMongoScript; -import org.springframework.lang.Nullable; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScrollUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScrollUtils.java index 85ddce7656..62e6d6c513 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScrollUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScrollUtils.java @@ -29,6 +29,7 @@ import org.springframework.data.domain.Window; import org.springframework.data.mongodb.core.EntityOperations.Entity; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.util.Assert; /** * Utilities to run scroll queries and create {@link Window} results. @@ -48,7 +49,11 @@ class ScrollUtils { */ static KeysetScrollQuery createKeysetPaginationQuery(Query query, String idPropertyName) { + KeysetScrollPosition keyset = query.getKeyset(); + + Assert.notNull(keyset, "Query.keyset must not be null"); + KeysetScrollDirector director = KeysetScrollDirector.of(keyset.getDirection()); Document sortObject = director.getSortObject(idPropertyName, query); Document fieldsObject = director.getFieldsObject(query.getFieldsObject(), sortObject); @@ -61,6 +66,9 @@ static Window createWindow(Query query, List result, Class sourceTy Document sortObject = query.getSortObject(); KeysetScrollPosition keyset = query.getKeyset(); + + Assert.notNull(keyset, "Query.keyset must not be null"); + Direction direction = keyset.getDirection(); KeysetScrollDirector director = KeysetScrollDirector.of(direction); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionCallback.java index 55a87ecadf..76a6d525f8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionCallback.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionCallback.java @@ -15,8 +15,8 @@ */ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.lang.Nullable; /** * Callback interface for executing operations within a {@link com.mongodb.session.ClientSession}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionScoped.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionScoped.java index 33ad9d7318..906d682685 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionScoped.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionScoped.java @@ -17,10 +17,10 @@ import java.util.function.Consumer; -import org.springframework.lang.Nullable; - import com.mongodb.client.ClientSession; +import org.jspecify.annotations.Nullable; + /** * Gateway interface to execute {@link ClientSession} bound operations against MongoDB via a {@link SessionCallback}. *
@@ -42,8 +42,7 @@ public interface SessionScoped { * @param return type. * @return a result object returned by the action. Can be {@literal null}. */ - @Nullable - default T execute(SessionCallback action) { + default @Nullable T execute(SessionCallback action) { return execute(action, session -> {}); } @@ -60,6 +59,5 @@ default T execute(SessionCallback action) { * @param return type. * @return a result object returned by the action. Can be {@literal null}. */ - @Nullable - T execute(SessionCallback action, Consumer doFinally); + @Nullable T execute(SessionCallback action, Consumer doFinally); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java index 84edf13d57..529f912e6c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java @@ -18,13 +18,13 @@ import reactor.core.publisher.Mono; import org.bson.codecs.configuration.CodecRegistry; +import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory; import org.springframework.data.mongodb.SessionAwareMethodInterceptor; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SortingQueryCursorPreparer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SortingQueryCursorPreparer.java index c69fb4ad15..1652dca259 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SortingQueryCursorPreparer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SortingQueryCursorPreparer.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@link CursorPreparer} that exposes its {@link Document sort document}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ViewOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ViewOptions.java index e50e1088cb..b4b525fc97 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ViewOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ViewOptions.java @@ -17,8 +17,9 @@ import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Collation; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; /** * Immutable object holding additional options to be applied when creating a MongoDB @@ -59,6 +60,7 @@ public Optional getCollation() { * @param collation the {@link Collation} to use for language-specific string comparison. * @return new instance of {@link ViewOptions}. */ + @Contract("_ -> new") public ViewOptions collation(Collation collation) { return new ViewOptions(collation); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernAware.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernAware.java index d6e4119b20..bdc7de6663 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernAware.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernAware.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import com.mongodb.WriteConcern; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernResolver.java index 8df4171844..a72c656e47 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernResolver.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import com.mongodb.WriteConcern; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java index d4cdece411..710b570ed7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java @@ -26,6 +26,7 @@ import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; @@ -282,7 +283,7 @@ protected T get(int index) { * @since 2.1 */ @SuppressWarnings("unchecked") - protected T get(Object key) { + protected @Nullable T get(Object key) { Assert.isInstanceOf(Map.class, this.value, "Value must be a type of Map"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java index cf6485c230..fa44656c99 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java @@ -22,6 +22,8 @@ import java.util.Map; import org.bson.Document; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -60,8 +62,8 @@ public static AccumulatorOperatorFactory valueOf(AggregationExpression expressio */ public static class AccumulatorOperatorFactory { - private final String fieldReference; - private final AggregationExpression expression; + private final @Nullable String fieldReference; + private final @Nullable AggregationExpression expression; /** * Creates new {@link AccumulatorOperatorFactory} for given {@literal fieldReference}. @@ -93,6 +95,7 @@ public AccumulatorOperatorFactory(AggregationExpression expression) { * * @return new instance of {@link Sum}. */ + @SuppressWarnings("NullAway") public Sum sum() { return usesFieldRef() ? Sum.sumOf(fieldReference) : Sum.sumOf(expression); } @@ -103,6 +106,7 @@ public Sum sum() { * * @return new instance of {@link Avg}. */ + @SuppressWarnings("NullAway") public Avg avg() { return usesFieldRef() ? Avg.avgOf(fieldReference) : Avg.avgOf(expression); } @@ -113,6 +117,7 @@ public Avg avg() { * * @return new instance of {@link Max}. */ + @SuppressWarnings("NullAway") public Max max() { return usesFieldRef() ? Max.maxOf(fieldReference) : Max.maxOf(expression); } @@ -134,6 +139,7 @@ public Max max(int numberOfResults) { * * @return new instance of {@link Min}. */ + @SuppressWarnings("NullAway") public Min min() { return usesFieldRef() ? Min.minOf(fieldReference) : Min.minOf(expression); } @@ -155,6 +161,7 @@ public Min min(int numberOfResults) { * * @return new instance of {@link StdDevPop}. */ + @SuppressWarnings("NullAway") public StdDevPop stdDevPop() { return usesFieldRef() ? StdDevPop.stdDevPopOf(fieldReference) : StdDevPop.stdDevPopOf(expression); } @@ -165,6 +172,7 @@ public StdDevPop stdDevPop() { * * @return new instance of {@link StdDevSamp}. */ + @SuppressWarnings("NullAway") public StdDevSamp stdDevSamp() { return usesFieldRef() ? StdDevSamp.stdDevSampOf(fieldReference) : StdDevSamp.stdDevSampOf(expression); } @@ -193,6 +201,7 @@ public CovariancePop covariancePop(AggregationExpression expression) { return covariancePop().and(expression); } + @SuppressWarnings("NullAway") private CovariancePop covariancePop() { return usesFieldRef() ? CovariancePop.covariancePopOf(fieldReference) : CovariancePop.covariancePopOf(expression); } @@ -221,6 +230,7 @@ public CovarianceSamp covarianceSamp(AggregationExpression expression) { return covarianceSamp().and(expression); } + @SuppressWarnings("NullAway") private CovarianceSamp covarianceSamp() { return usesFieldRef() ? CovarianceSamp.covarianceSampOf(fieldReference) : CovarianceSamp.covarianceSampOf(expression); @@ -233,6 +243,7 @@ private CovarianceSamp covarianceSamp() { * @return new instance of {@link ExpMovingAvg}. * @since 3.3 */ + @SuppressWarnings("NullAway") public ExpMovingAvgBuilder expMovingAvg() { ExpMovingAvg expMovingAvg = usesFieldRef() ? ExpMovingAvg.expMovingAvgOf(fieldReference) @@ -252,13 +263,14 @@ public ExpMovingAvg alpha(double exponentialDecayValue) { } /** - * Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the - * associated numeric value expression. + * Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the associated numeric + * value expression. * * @return new instance of {@link Percentile}. * @param percentages must not be {@literal null}. * @since 4.2 */ + @SuppressWarnings("NullAway") public Percentile percentile(Double... percentages) { Percentile percentile = usesFieldRef() ? Percentile.percentileOf(fieldReference) : Percentile.percentileOf(expression); @@ -271,6 +283,7 @@ public Percentile percentile(Double... percentages) { * @return new instance of {@link Median}. * @since 4.2 */ + @SuppressWarnings("NullAway") public Median median() { return usesFieldRef() ? Median.medianOf(fieldReference) : Median.medianOf(expression); } @@ -339,6 +352,7 @@ public static Sum sumOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Sum}. */ + @Contract("_ -> new") public static Sum sumOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -352,6 +366,7 @@ public static Sum sumOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Sum}. */ + @Contract("_ -> new") public Sum and(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -365,6 +380,7 @@ public Sum and(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Sum}. */ + @Contract("_ -> new") public Sum and(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -379,6 +395,7 @@ public Sum and(AggregationExpression expression) { * @return new instance of {@link Sum}. * @since 2.2 */ + @Contract("_ -> new") public Sum and(Number value) { Assert.notNull(value, "Value must not be null"); @@ -386,7 +403,6 @@ public Sum and(Number value) { } @Override - @SuppressWarnings("unchecked") public Document toDocument(Object value, AggregationOperationContext context) { if (value instanceof List list && list.size() == 1) { @@ -444,6 +460,7 @@ public static Avg avgOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Avg}. */ + @Contract("_ -> new") public Avg and(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -457,6 +474,7 @@ public Avg and(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Avg}. */ + @Contract("_ -> new") public Avg and(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -464,7 +482,6 @@ public Avg and(AggregationExpression expression) { } @Override - @SuppressWarnings("unchecked") public Document toDocument(Object value, AggregationOperationContext context) { if (value instanceof List list && list.size() == 1) { @@ -522,6 +539,7 @@ public static Max maxOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Max}. */ + @Contract("_ -> new") public Max and(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -535,6 +553,7 @@ public Max and(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Max}. */ + @Contract("_ -> new") public Max and(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -548,11 +567,13 @@ public Max and(AggregationExpression expression) { * @param numberOfResults * @return new instance of {@link Max}. */ + @Contract("_ -> new") public Max limit(int numberOfResults) { return new Max(append("n", numberOfResults)); } @Override + @SuppressWarnings("NullAway") public Document toDocument(AggregationOperationContext context) { if (get("n") == null) { return toDocument(get("input"), context); @@ -619,6 +640,7 @@ public static Min minOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Min}. */ + @Contract("_ -> new") public Min and(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -632,6 +654,7 @@ public Min and(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Min}. */ + @Contract("_ -> new") public Min and(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -645,11 +668,13 @@ public Min and(AggregationExpression expression) { * @param numberOfResults * @return new instance of {@link Min}. */ + @Contract("_ -> new") public Min limit(int numberOfResults) { return new Min(append("n", numberOfResults)); } @Override + @SuppressWarnings("NullAway") public Document toDocument(AggregationOperationContext context) { if (get("n") == null) { @@ -659,7 +684,6 @@ public Document toDocument(AggregationOperationContext context) { } @Override - @SuppressWarnings("unchecked") public Document toDocument(Object value, AggregationOperationContext context) { if (value instanceof List list && list.size() == 1) { @@ -717,6 +741,7 @@ public static StdDevPop stdDevPopOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link StdDevPop}. */ + @Contract("_ -> new") public StdDevPop and(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -730,6 +755,7 @@ public StdDevPop and(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link StdDevPop}. */ + @Contract("_ -> new") public StdDevPop and(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -737,7 +763,6 @@ public StdDevPop and(AggregationExpression expression) { } @Override - @SuppressWarnings("unchecked") public Document toDocument(Object value, AggregationOperationContext context) { if (value instanceof List list && list.size() == 1) { @@ -795,6 +820,7 @@ public static StdDevSamp stdDevSampOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link StdDevSamp}. */ + @Contract("_ -> new") public StdDevSamp and(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -808,6 +834,7 @@ public StdDevSamp and(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link StdDevSamp}. */ + @Contract("_ -> new") public StdDevSamp and(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -815,7 +842,6 @@ public StdDevSamp and(AggregationExpression expression) { } @Override - @SuppressWarnings("unchecked") public Document toDocument(Object value, AggregationOperationContext context) { if (value instanceof List list && list.size() == 1) { @@ -866,6 +892,7 @@ public static CovariancePop covariancePopOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link CovariancePop}. */ + @Contract("_ -> new") public CovariancePop and(String fieldReference) { return new CovariancePop(append(asFields(fieldReference))); } @@ -876,6 +903,7 @@ public CovariancePop and(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link CovariancePop}. */ + @Contract("_ -> new") public CovariancePop and(AggregationExpression expression) { return new CovariancePop(append(expression)); } @@ -926,6 +954,7 @@ public static CovarianceSamp covarianceSampOf(AggregationExpression expression) * @param fieldReference must not be {@literal null}. * @return new instance of {@link CovarianceSamp}. */ + @Contract("_ -> new") public CovarianceSamp and(String fieldReference) { return new CovarianceSamp(append(asFields(fieldReference))); } @@ -936,6 +965,7 @@ public CovarianceSamp and(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link CovarianceSamp}. */ + @Contract("_ -> new") public CovarianceSamp and(AggregationExpression expression) { return new CovarianceSamp(append(expression)); } @@ -986,6 +1016,7 @@ public static ExpMovingAvg expMovingAvgOf(AggregationExpression expression) { * @param numberOfHistoricalDocuments * @return new instance of {@link ExpMovingAvg}. */ + @Contract("_ -> new") public ExpMovingAvg n/*umber of historical documents*/(int numberOfHistoricalDocuments) { return new ExpMovingAvg(append("N", numberOfHistoricalDocuments)); } @@ -997,6 +1028,7 @@ public static ExpMovingAvg expMovingAvgOf(AggregationExpression expression) { * @param exponentialDecayValue * @return new instance of {@link ExpMovingAvg}. */ + @Contract("_ -> new") public ExpMovingAvg alpha(double exponentialDecayValue) { return new ExpMovingAvg(append("alpha", exponentialDecayValue)); } @@ -1055,6 +1087,7 @@ public static Percentile percentileOf(AggregationExpression expression) { * @param percentages must not be {@literal null}. * @return new instance of {@link Percentile}. */ + @Contract("_ -> new") public Percentile percentages(Double... percentages) { Assert.notEmpty(percentages, "Percentages must not be null or empty"); @@ -1068,6 +1101,7 @@ public Percentile percentages(Double... percentages) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Percentile}. */ + @Contract("_ -> new") public Percentile and(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1081,6 +1115,7 @@ public Percentile and(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Percentile}. */ + @Contract("_ -> new") public Percentile and(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1142,6 +1177,7 @@ public static Median medianOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Median}. */ + @Contract("_ -> new") public Median and(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1155,6 +1191,7 @@ public Median and(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Median}. */ + @Contract("_ -> new") public Median and(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperation.java index 0dc1588bf8..e76ebb894d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperation.java @@ -19,8 +19,9 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.AddFieldsOperation.AddFieldsOperationBuilder.ValueAppender; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; /** * Adds new fields to documents. {@code $addFields} outputs documents that contain all existing fields from the input @@ -82,6 +83,7 @@ public static ValueAppender addField(String field) { * @param value the value to assign. * @return new instance of {@link AddFieldsOperation}. */ + @Contract("_ -> new") public AddFieldsOperation addField(Object field, Object value) { LinkedHashMap target = new LinkedHashMap<>(getValueMap()); @@ -95,6 +97,7 @@ public AddFieldsOperation addField(Object field, Object value) { * * @return new instance of {@link AddFieldsOperationBuilder}. */ + @Contract("-> new") public AddFieldsOperationBuilder and() { return new AddFieldsOperationBuilder(getValueMap()); } @@ -139,7 +142,7 @@ public ValueAppender addField(String field) { return new ValueAppender() { @Override - public AddFieldsOperationBuilder withValue(Object value) { + public AddFieldsOperationBuilder withValue(@Nullable Object value) { valueMap.put(field, value); return AddFieldsOperationBuilder.this; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionTransformer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionTransformer.java index 00db38329f..e33c565d11 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionTransformer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionTransformer.java @@ -16,12 +16,12 @@ package org.springframework.data.mongodb.core.aggregation; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.AggregationExpressionTransformer.AggregationExpressionTransformationContext; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; import org.springframework.data.mongodb.core.spel.ExpressionNode; import org.springframework.data.mongodb.core.spel.ExpressionTransformationContextSupport; import org.springframework.data.mongodb.core.spel.ExpressionTransformer; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java index a49c7e46d5..5027328461 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java @@ -21,10 +21,10 @@ import org.bson.Document; import org.bson.codecs.configuration.CodecRegistry; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; import org.springframework.data.mongodb.CodecRegistryProvider; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java index fd5f7ed979..6437ec981d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java @@ -19,12 +19,12 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; import org.springframework.data.mongodb.core.aggregation.Fields.AggregationField; import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation; -import org.springframework.lang.Nullable; /** * Rendering support for {@link AggregationOperation} into a {@link List} of {@link org.bson.Document}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java index 327d40b8c7..278da408c6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java @@ -19,11 +19,12 @@ import java.util.Optional; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.ReadConcernAware; import org.springframework.data.mongodb.core.ReadPreferenceAware; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import com.mongodb.ReadConcern; @@ -299,7 +300,7 @@ public boolean hasReadConcern() { } @Override - public ReadConcern getReadConcern() { + public @Nullable ReadConcern getReadConcern() { return readConcern.orElse(null); } @@ -309,7 +310,7 @@ public boolean hasReadPreference() { } @Override - public ReadPreference getReadPreference() { + public @Nullable ReadPreference getReadPreference() { return readPreference.orElse(null); } @@ -426,7 +427,7 @@ static Document createCursor(int cursorBatchSize) { */ public static class Builder { - private Boolean allowDiskUse; + private @Nullable Boolean allowDiskUse; private boolean explain; private @Nullable Document cursor; private @Nullable Collation collation; @@ -444,6 +445,7 @@ public static class Builder { * @param allowDiskUse use {@literal true} to allow disk use during the aggregation. * @return this. */ + @Contract("_ -> this") public Builder allowDiskUse(boolean allowDiskUse) { this.allowDiskUse = allowDiskUse; @@ -456,6 +458,7 @@ public Builder allowDiskUse(boolean allowDiskUse) { * @param explain use {@literal true} to enable explain feature. * @return this. */ + @Contract("_ -> this") public Builder explain(boolean explain) { this.explain = explain; @@ -468,6 +471,7 @@ public Builder explain(boolean explain) { * @param cursor must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Builder cursor(Document cursor) { this.cursor = cursor; @@ -481,6 +485,7 @@ public Builder cursor(Document cursor) { * @return this. * @since 2.0 */ + @Contract("_ -> this") public Builder cursorBatchSize(int batchSize) { this.cursor = createCursor(batchSize); @@ -494,6 +499,7 @@ public Builder cursorBatchSize(int batchSize) { * @return this. * @since 2.0 */ + @Contract("_ -> this") public Builder collation(@Nullable Collation collation) { this.collation = collation; @@ -507,6 +513,7 @@ public Builder collation(@Nullable Collation collation) { * @return this. * @since 2.2 */ + @Contract("_ -> this") public Builder comment(@Nullable String comment) { this.comment = comment; @@ -520,6 +527,7 @@ public Builder comment(@Nullable String comment) { * @return this. * @since 3.1 */ + @Contract("_ -> this") public Builder hint(@Nullable Document hint) { this.hint = hint; @@ -533,6 +541,7 @@ public Builder hint(@Nullable Document hint) { * @return this. * @since 4.1 */ + @Contract("_ -> this") public Builder hint(@Nullable String indexName) { this.hint = indexName; @@ -546,6 +555,7 @@ public Builder hint(@Nullable String indexName) { * @return this. * @since 4.1 */ + @Contract("_ -> this") public Builder readConcern(@Nullable ReadConcern readConcern) { this.readConcern = readConcern; @@ -559,6 +569,7 @@ public Builder readConcern(@Nullable ReadConcern readConcern) { * @return this. * @since 4.1 */ + @Contract("_ -> this") public Builder readPreference(@Nullable ReadPreference readPreference) { this.readPreference = readPreference; @@ -573,6 +584,7 @@ public Builder readPreference(@Nullable ReadPreference readPreference) { * @return this. * @since 3.0 */ + @Contract("_ -> this") public Builder maxTime(@Nullable Duration maxTime) { this.maxTime = maxTime; @@ -587,6 +599,7 @@ public Builder maxTime(@Nullable Duration maxTime) { * @return this. * @since 3.0.2 */ + @Contract("-> this") public Builder skipOutput() { this.resultOptions = ResultOptions.SKIP; @@ -600,6 +613,7 @@ public Builder skipOutput() { * @return this. * @since 3.2 */ + @Contract("-> this") public Builder strictMapping() { this.domainTypeMapping = DomainTypeMapping.STRICT; @@ -613,6 +627,7 @@ public Builder strictMapping() { * @return this. * @since 3.2 */ + @Contract("-> this") public Builder relaxedMapping() { this.domainTypeMapping = DomainTypeMapping.RELAXED; @@ -625,6 +640,7 @@ public Builder relaxedMapping() { * @return this. * @since 3.2 */ + @Contract("-> this") public Builder noMapping() { this.domainTypeMapping = DomainTypeMapping.NONE; @@ -636,6 +652,7 @@ public Builder noMapping() { * * @return new instance of {@link AggregationOptions}. */ + @Contract("-> new") public AggregationOptions build() { AggregationOptions options = new AggregationOptions(allowDiskUse, explain, cursor, collation, comment, hint); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationPipeline.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationPipeline.java index 68662ec0df..f06803997b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationPipeline.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationPipeline.java @@ -22,7 +22,10 @@ import java.util.function.Predicate; import org.bson.Document; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; /** * The {@link AggregationPipeline} holds the collection of {@link AggregationOperation aggregation stages}. @@ -63,6 +66,7 @@ public AggregationPipeline(List aggregationOperations) { * @param aggregationOperation must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public AggregationPipeline add(AggregationOperation aggregationOperation) { Assert.notNull(aggregationOperation, "AggregationOperation must not be null"); @@ -80,6 +84,14 @@ public List getOperations() { return Collections.unmodifiableList(pipeline); } + public @Nullable AggregationOperation firstOperation() { + return CollectionUtils.firstElement(pipeline); + } + + public @Nullable AggregationOperation lastOperation() { + return CollectionUtils.lastElement(pipeline); + } + List toDocuments(AggregationOperationContext context) { verify(); @@ -95,8 +107,8 @@ public boolean isOutOrMerge() { return false; } - AggregationOperation operation = pipeline.get(pipeline.size() - 1); - return isOut(operation) || isMerge(operation); + AggregationOperation operation = lastOperation(); + return operation != null && (isOut(operation) || isMerge(operation)); } void verify() { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java index 438eb9e49f..7b27739229 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java @@ -20,7 +20,7 @@ import java.util.List; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** @@ -71,8 +71,7 @@ public List getMappedResults() { * @return the single already mapped result object or raise an error if more than one found. * @throws IllegalArgumentException in case more than one result is available. */ - @Nullable - public T getUniqueMappedResult() { + public @Nullable T getUniqueMappedResult() { Assert.isTrue(mappedResults.size() < 2, "Expected unique result or null, but got more than one"); return mappedResults.size() == 1 ? mappedResults.get(0) : null; } @@ -101,10 +100,10 @@ public Document getRawResults() { return rawResults; } - @Nullable - private String parseServerUsed() { + private @Nullable String parseServerUsed() { Object object = rawResults.get("serverUsed"); return object instanceof String stringValue ? stringValue : null; } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationSpELExpression.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationSpELExpression.java index 1626d672bc..c5b53ef0c6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationSpELExpression.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationSpELExpression.java @@ -66,6 +66,8 @@ public static AggregationSpELExpression expressionOf(String expressionString, Ob @Override public Document toDocument(AggregationOperationContext context) { - return (Document) TRANSFORMER.transform(rawExpression, context, parameters); + + Document doc = (Document) TRANSFORMER.transform(rawExpression, context, parameters); + return doc != null ? doc : new Document(); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdate.java index 15d700309e..9e8564c03e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdate.java @@ -25,11 +25,11 @@ import java.util.stream.Collectors; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.SerializationUtils; import org.springframework.data.mongodb.core.query.UpdateDefinition; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -129,6 +129,7 @@ public static AggregationUpdate from(List pipeline) { * @return this. * @see $set Aggregation Reference */ + @Contract("_ -> this") public AggregationUpdate set(SetOperation setOperation) { Assert.notNull(setOperation, "SetOperation must not be null"); @@ -148,6 +149,7 @@ public AggregationUpdate set(SetOperation setOperation) { * @see $unset Aggregation * Reference */ + @Contract("_ -> this") public AggregationUpdate unset(UnsetOperation unsetOperation) { Assert.notNull(unsetOperation, "UnsetOperation must not be null"); @@ -166,6 +168,7 @@ public AggregationUpdate unset(UnsetOperation unsetOperation) { * @see $replaceWith Aggregation * Reference */ + @Contract("_ -> this") public AggregationUpdate replaceWith(ReplaceWithOperation replaceWithOperation) { Assert.notNull(replaceWithOperation, "ReplaceWithOperation must not be null"); @@ -179,6 +182,7 @@ public AggregationUpdate replaceWith(ReplaceWithOperation replaceWithOperation) * @param value must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public AggregationUpdate replaceWith(Object value) { Assert.notNull(value, "Value must not be null"); @@ -193,6 +197,7 @@ public AggregationUpdate replaceWith(Object value) { * @return new instance of {@link SetValueAppender}. * @see #set(SetOperation) */ + @Contract("_ -> new") public SetValueAppender set(String key) { Assert.notNull(key, "Key must not be null"); @@ -219,6 +224,7 @@ public AggregationUpdate toValueOf(Object value) { * @param keys the fields to remove. * @return this. */ + @Contract("_ -> this") public AggregationUpdate unset(String... keys) { Assert.notNull(keys, "Keys must not be null"); @@ -234,6 +240,7 @@ public AggregationUpdate unset(String... keys) { * * @return never {@literal null}. */ + @Contract("-> this") public AggregationUpdate isolated() { isolated = true; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationVariable.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationVariable.java index ed79202345..522dd5eae5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationVariable.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationVariable.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.aggregation; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java index e2c31c6346..c7787b382c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java @@ -20,6 +20,7 @@ import java.util.Locale; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Avg; import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.CovariancePop; import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.CovarianceSamp; @@ -32,7 +33,7 @@ import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Sum; import org.springframework.data.mongodb.core.aggregation.SetWindowFieldsOperation.WindowUnit; import org.springframework.data.mongodb.core.aggregation.SetWindowFieldsOperation.WindowUnits; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -84,8 +85,8 @@ public static Rand rand() { */ public static class ArithmeticOperatorFactory { - private final String fieldReference; - private final AggregationExpression expression; + private final @Nullable String fieldReference; + private final @Nullable AggregationExpression expression; /** * Creates new {@link ArithmeticOperatorFactory} for given {@literal fieldReference}. @@ -116,6 +117,7 @@ public ArithmeticOperatorFactory(AggregationExpression expression) { * * @return new instance of {@link Abs}. */ + @SuppressWarnings("NullAway") public Abs abs() { return usesFieldRef() ? Abs.absoluteValueOf(fieldReference) : Abs.absoluteValueOf(expression); } @@ -158,6 +160,7 @@ public Add add(Number value) { return createAdd().add(value); } + @SuppressWarnings("NullAway") private Add createAdd() { return usesFieldRef() ? Add.valueOf(fieldReference) : Add.valueOf(expression); } @@ -168,6 +171,7 @@ private Add createAdd() { * * @return new instance of {@link Ceil}. */ + @SuppressWarnings("NullAway") public Ceil ceil() { return usesFieldRef() ? Ceil.ceilValueOf(fieldReference) : Ceil.ceilValueOf(expression); } @@ -205,6 +209,7 @@ public Derivative derivative(WindowUnit unit) { * @return new instance of {@link Derivative}. * @since 3.3 */ + @SuppressWarnings("NullAway") public Derivative derivative(@Nullable String unit) { Derivative derivative = usesFieldRef() ? Derivative.derivativeOf(fieldReference) @@ -250,6 +255,7 @@ public Divide divideBy(Number value) { return createDivide().divideBy(value); } + @SuppressWarnings("NullAway") private Divide createDivide() { return usesFieldRef() ? Divide.valueOf(fieldReference) : Divide.valueOf(expression); } @@ -259,6 +265,7 @@ private Divide createDivide() { * * @return new instance of {@link Exp}. */ + @SuppressWarnings("NullAway") public Exp exp() { return usesFieldRef() ? Exp.expValueOf(fieldReference) : Exp.expValueOf(expression); } @@ -269,6 +276,7 @@ public Exp exp() { * * @return new instance of {@link Floor}. */ + @SuppressWarnings("NullAway") public Floor floor() { return usesFieldRef() ? Floor.floorValueOf(fieldReference) : Floor.floorValueOf(expression); } @@ -279,6 +287,7 @@ public Floor floor() { * @return new instance of {@link Integral}. * @since 3.3 */ + @SuppressWarnings("NullAway") public Integral integral() { return usesFieldRef() ? Integral.integralOf(fieldReference) : Integral.integralOf(expression); } @@ -318,6 +327,7 @@ public Integral integral(String unit) { * * @return new instance of {@link Ln}. */ + @SuppressWarnings("NullAway") public Ln ln() { return usesFieldRef() ? Ln.lnValueOf(fieldReference) : Ln.lnValueOf(expression); } @@ -345,7 +355,7 @@ public Log log(String fieldReference) { public Log log(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); - return createLog().log(fieldReference); + return createLog().log(expression); } /** @@ -361,6 +371,7 @@ public Log log(Number base) { return createLog().log(base); } + @SuppressWarnings("NullAway") private Log createLog() { return usesFieldRef() ? Log.valueOf(fieldReference) : Log.valueOf(expression); } @@ -370,6 +381,7 @@ private Log createLog() { * * @return new instance of {@link Log10}. */ + @SuppressWarnings("NullAway") public Log10 log10() { return usesFieldRef() ? Log10.log10ValueOf(fieldReference) : Log10.log10ValueOf(expression); } @@ -413,6 +425,7 @@ public Mod mod(Number value) { return createMod().mod(value); } + @SuppressWarnings("NullAway") private Mod createMod() { return usesFieldRef() ? Mod.valueOf(fieldReference) : Mod.valueOf(expression); } @@ -453,6 +466,7 @@ public Multiply multiplyBy(Number value) { return createMultiply().multiplyBy(value); } + @SuppressWarnings("NullAway") private Multiply createMultiply() { return usesFieldRef() ? Multiply.valueOf(fieldReference) : Multiply.valueOf(expression); } @@ -493,6 +507,7 @@ public Pow pow(Number value) { return createPow().pow(value); } + @SuppressWarnings("NullAway") private Pow createPow() { return usesFieldRef() ? Pow.valueOf(fieldReference) : Pow.valueOf(expression); } @@ -502,6 +517,7 @@ private Pow createPow() { * * @return new instance of {@link Sqrt}. */ + @SuppressWarnings("NullAway") public Sqrt sqrt() { return usesFieldRef() ? Sqrt.sqrtOf(fieldReference) : Sqrt.sqrtOf(expression); } @@ -542,6 +558,7 @@ public Subtract subtract(Number value) { return createSubtract().subtract(value); } + @SuppressWarnings("NullAway") private Subtract createSubtract() { return usesFieldRef() ? Subtract.valueOf(fieldReference) : Subtract.valueOf(expression); } @@ -551,6 +568,7 @@ private Subtract createSubtract() { * * @return new instance of {@link Trunc}. */ + @SuppressWarnings("NullAway") public Trunc trunc() { return usesFieldRef() ? Trunc.truncValueOf(fieldReference) : Trunc.truncValueOf(expression); } @@ -560,6 +578,7 @@ public Trunc trunc() { * * @return new instance of {@link Sum}. */ + @SuppressWarnings("NullAway") public Sum sum() { return usesFieldRef() ? AccumulatorOperators.Sum.sumOf(fieldReference) : AccumulatorOperators.Sum.sumOf(expression); @@ -570,6 +589,7 @@ public Sum sum() { * * @return new instance of {@link Avg}. */ + @SuppressWarnings("NullAway") public Avg avg() { return usesFieldRef() ? AccumulatorOperators.Avg.avgOf(fieldReference) : AccumulatorOperators.Avg.avgOf(expression); @@ -580,6 +600,7 @@ public Avg avg() { * * @return new instance of {@link Max}. */ + @SuppressWarnings("NullAway") public Max max() { return usesFieldRef() ? AccumulatorOperators.Max.maxOf(fieldReference) : AccumulatorOperators.Max.maxOf(expression); @@ -590,6 +611,7 @@ public Max max() { * * @return new instance of {@link Min}. */ + @SuppressWarnings("NullAway") public Min min() { return usesFieldRef() ? AccumulatorOperators.Min.minOf(fieldReference) : AccumulatorOperators.Min.minOf(expression); @@ -600,6 +622,7 @@ public Min min() { * * @return new instance of {@link StdDevPop}. */ + @SuppressWarnings("NullAway") public StdDevPop stdDevPop() { return usesFieldRef() ? AccumulatorOperators.StdDevPop.stdDevPopOf(fieldReference) : AccumulatorOperators.StdDevPop.stdDevPopOf(expression); @@ -610,6 +633,7 @@ public StdDevPop stdDevPop() { * * @return new instance of {@link StdDevSamp}. */ + @SuppressWarnings("NullAway") public StdDevSamp stdDevSamp() { return usesFieldRef() ? AccumulatorOperators.StdDevSamp.stdDevSampOf(fieldReference) : AccumulatorOperators.StdDevSamp.stdDevSampOf(expression); @@ -639,6 +663,7 @@ public CovariancePop covariancePop(AggregationExpression expression) { return covariancePop().and(expression); } + @SuppressWarnings("NullAway") private CovariancePop covariancePop() { return usesFieldRef() ? CovariancePop.covariancePopOf(fieldReference) : CovariancePop.covariancePopOf(expression); } @@ -667,6 +692,7 @@ public CovarianceSamp covarianceSamp(AggregationExpression expression) { return covarianceSamp().and(expression); } + @SuppressWarnings("NullAway") private CovarianceSamp covarianceSamp() { return usesFieldRef() ? CovarianceSamp.covarianceSampOf(fieldReference) : CovarianceSamp.covarianceSampOf(expression); @@ -679,6 +705,7 @@ private CovarianceSamp covarianceSamp() { * @return new instance of {@link Round}. * @since 3.0 */ + @SuppressWarnings("NullAway") public Round round() { return usesFieldRef() ? Round.roundValueOf(fieldReference) : Round.roundValueOf(expression); } @@ -712,6 +739,7 @@ public Sin sin() { * @return new instance of {@link Sin}. * @since 3.3 */ + @SuppressWarnings("NullAway") public Sin sin(AngularUnit unit) { return usesFieldRef() ? Sin.sinOf(fieldReference, unit) : Sin.sinOf(expression, unit); } @@ -734,6 +762,7 @@ public Sinh sinh() { * @return new instance of {@link Sinh}. * @since 3.3 */ + @SuppressWarnings("NullAway") public Sinh sinh(AngularUnit unit) { return usesFieldRef() ? Sinh.sinhOf(fieldReference, unit) : Sinh.sinhOf(expression, unit); } @@ -744,6 +773,7 @@ public Sinh sinh(AngularUnit unit) { * @return new instance of {@link ASin}. * @since 3.3 */ + @SuppressWarnings("NullAway") public ASin asin() { return usesFieldRef() ? ASin.asinOf(fieldReference) : ASin.asinOf(expression); } @@ -754,6 +784,7 @@ public ASin asin() { * @return new instance of {@link ASinh}. * @since 3.3 */ + @SuppressWarnings("NullAway") public ASinh asinh() { return usesFieldRef() ? ASinh.asinhOf(fieldReference) : ASinh.asinhOf(expression); } @@ -777,6 +808,7 @@ public Cos cos() { * @return new instance of {@link Cos}. * @since 3.3 */ + @SuppressWarnings("NullAway") public Cos cos(AngularUnit unit) { return usesFieldRef() ? Cos.cosOf(fieldReference, unit) : Cos.cosOf(expression, unit); } @@ -799,6 +831,7 @@ public Cosh cosh() { * @return new instance of {@link Cosh}. * @since 3.3 */ + @SuppressWarnings("NullAway") public Cosh cosh(AngularUnit unit) { return usesFieldRef() ? Cosh.coshOf(fieldReference, unit) : Cosh.coshOf(expression, unit); } @@ -809,6 +842,7 @@ public Cosh cosh(AngularUnit unit) { * @return new instance of {@link ACos}. * @since 3.4 */ + @SuppressWarnings("NullAway") public ACos acos() { return usesFieldRef() ? ACos.acosOf(fieldReference) : ACos.acosOf(expression); } @@ -819,6 +853,7 @@ public ACos acos() { * @return new instance of {@link ACosh}. * @since 3.4 */ + @SuppressWarnings("NullAway") public ACosh acosh() { return usesFieldRef() ? ACosh.acoshOf(fieldReference) : ACosh.acoshOf(expression); } @@ -840,6 +875,7 @@ public Tan tan() { * @return new instance of {@link ATan}. * @since 3.3 */ + @SuppressWarnings("NullAway") public ATan atan() { return usesFieldRef() ? ATan.atanOf(fieldReference) : ATan.atanOf(expression); } @@ -852,6 +888,7 @@ public ATan atan() { * @return new instance of {@link ATan2}. * @since 3.3 */ + @SuppressWarnings("NullAway") public ATan2 atan2(Number value) { Assert.notNull(value, "Value must not be null"); @@ -886,8 +923,8 @@ public ATan2 atan2(AggregationExpression expression) { return createATan2().atan2of(expression); } + @SuppressWarnings("NullAway") private ATan2 createATan2() { - return usesFieldRef() ? ATan2.valueOf(fieldReference) : ATan2.valueOf(expression); } @@ -897,6 +934,7 @@ private ATan2 createATan2() { * @return new instance of {@link ATanh}. * @since 3.3 */ + @SuppressWarnings("NullAway") public ATanh atanh() { return usesFieldRef() ? ATanh.atanhOf(fieldReference) : ATanh.atanhOf(expression); } @@ -909,6 +947,7 @@ public ATanh atanh() { * @return new instance of {@link Tan}. * @since 3.3 */ + @SuppressWarnings("NullAway") public Tan tan(AngularUnit unit) { return usesFieldRef() ? Tan.tanOf(fieldReference, unit) : Tan.tanOf(expression, unit); } @@ -931,18 +970,19 @@ public Tanh tanh() { * @return new instance of {@link Tanh}. * @since 3.3 */ + @SuppressWarnings("NullAway") public Tanh tanh(AngularUnit unit) { return usesFieldRef() ? Tanh.tanhOf(fieldReference, unit) : Tanh.tanhOf(expression, unit); } /** - * Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the - * numeric value. + * Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the numeric value. * * @return new instance of {@link Percentile}. * @param percentages must not be {@literal null}. * @since 4.2 */ + @SuppressWarnings("NullAway") public Percentile percentile(Double... percentages) { Percentile percentile = usesFieldRef() ? AccumulatorOperators.Percentile.percentileOf(fieldReference) : AccumulatorOperators.Percentile.percentileOf(expression); @@ -950,12 +990,12 @@ public Percentile percentile(Double... percentages) { } /** - * Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the - * numeric value. + * Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the numeric value. * * @return new instance of {@link Median}. * @since 4.2 */ + @SuppressWarnings("NullAway") public Median median() { return usesFieldRef() ? AccumulatorOperators.Median.medianOf(fieldReference) : AccumulatorOperators.Median.medianOf(expression); @@ -1077,6 +1117,7 @@ public static Add valueOf(Number value) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Add}. */ + @Contract("_ -> new") public Add add(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1089,6 +1130,7 @@ public Add add(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Add}. */ + @Contract("_ -> new") public Add add(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1101,6 +1143,7 @@ public Add add(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Add}. */ + @Contract("_ -> new") public Add add(Number value) { return new Add(append(value)); } @@ -1217,6 +1260,7 @@ public static Divide valueOf(Number value) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Divide}. */ + @Contract("_ -> new") public Divide divideBy(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1229,6 +1273,7 @@ public Divide divideBy(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Divide}. */ + @Contract("_ -> new") public Divide divideBy(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1241,6 +1286,7 @@ public Divide divideBy(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Divide}. */ + @Contract("_ -> new") public Divide divideBy(Number value) { return new Divide(append(value)); } @@ -1463,6 +1509,7 @@ public static Log valueOf(Number value) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Log}. */ + @Contract("_ -> new") public Log log(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1475,6 +1522,7 @@ public Log log(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Log}. */ + @Contract("_ -> new") public Log log(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1487,6 +1535,7 @@ public Log log(AggregationExpression expression) { * @param base must not be {@literal null}. * @return new instance of {@link Log}. */ + @Contract("_ -> new") public Log log(Number base) { return new Log(append(base)); } @@ -1603,6 +1652,7 @@ public static Mod valueOf(Number value) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Mod}. */ + @Contract("_ -> new") public Mod mod(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1615,6 +1665,7 @@ public Mod mod(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Mod}. */ + @Contract("_ -> new") public Mod mod(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1627,6 +1678,7 @@ public Mod mod(AggregationExpression expression) { * @param base must not be {@literal null}. * @return new instance of {@link Mod}. */ + @Contract("_ -> new") public Mod mod(Number base) { return new Mod(append(base)); } @@ -1690,6 +1742,7 @@ public static Multiply valueOf(Number value) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Multiply}. */ + @Contract("_ -> new") public Multiply multiplyBy(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1702,6 +1755,7 @@ public Multiply multiplyBy(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Multiply}. */ + @Contract("_ -> new") public Multiply multiplyBy(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1714,6 +1768,7 @@ public Multiply multiplyBy(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Multiply}. */ + @Contract("_ -> new") public Multiply multiplyBy(Number value) { return new Multiply(append(value)); } @@ -1777,6 +1832,7 @@ public static Pow valueOf(Number value) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Pow}. */ + @Contract("_ -> new") public Pow pow(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1789,6 +1845,7 @@ public Pow pow(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Pow}. */ + @Contract("_ -> new") public Pow pow(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1801,6 +1858,7 @@ public Pow pow(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Pow}. */ + @Contract("_ -> new") public Pow pow(Number value) { return new Pow(append(value)); } @@ -1917,6 +1975,7 @@ public static Subtract valueOf(Number value) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Pow}. */ + @Contract("_ -> new") public Subtract subtract(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1929,6 +1988,7 @@ public Subtract subtract(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Pow}. */ + @Contract("_ -> new") public Subtract subtract(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1941,6 +2001,7 @@ public Subtract subtract(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Pow}. */ + @Contract("_ -> new") public Subtract subtract(Number value) { return new Subtract(append(value)); } @@ -2060,6 +2121,7 @@ public static Round round(Number value) { * @param place value between -20 and 100, exclusive. * @return new instance of {@link Round}. */ + @Contract("_ -> new") public Round place(int place) { return new Round(append(place)); } @@ -2070,6 +2132,7 @@ public Round place(int place) { * @param expression must not be {@literal null}. * @return new instance of {@link Round}. */ + @Contract("_ -> new") public Round placeOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -2083,6 +2146,7 @@ public Round placeOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Round}. */ + @Contract("_ -> new") public Round placeOf(String fieldReference) { Assert.notNull(fieldReference, "fieldReference must not be null"); @@ -2133,6 +2197,7 @@ public static Derivative derivativeOfValue(Number value) { return new Derivative(Collections.singletonMap("input", value)); } + @Contract("_ -> new") public Derivative unit(String unit) { return new Derivative(append("unit", unit)); } @@ -2183,6 +2248,7 @@ public static Integral integralOf(AggregationExpression expression) { * @param unit the unit of measure. * @return new instance of {@link Integral}. */ + @Contract("_ -> new") public Integral unit(String unit) { return new Integral(append("unit", unit)); } @@ -2217,8 +2283,7 @@ private Sin(Object value) { /** * Creates a new {@link AggregationExpression} that calculates the sine of a value that is measured in - * {@link AngularUnit#RADIANS radians}. - *
+ * {@link AngularUnit#RADIANS radians}.
* Use {@code sinhOf("angle", DEGREES)} as shortcut for * *
@@ -2330,8 +2395,7 @@ public static Sinh sinhOf(String fieldReference) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic sine of a value that is measured in
-		 * the given {@link AngularUnit unit}.
-		 * 
+ * the given {@link AngularUnit unit}.
* Use {@code sinhOf("angle", DEGREES)} as shortcut for * *
@@ -2350,8 +2414,7 @@ public static Sinh sinhOf(String fieldReference, AngularUnit unit) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic sine of a value that is measured in
-		 * {@link AngularUnit#RADIANS}.
-		 * 
+ * {@link AngularUnit#RADIANS}.
* Use {@code sinhOf("angle", DEGREES)} as shortcut for eg. * {@code sinhOf(ConvertOperators.valueOf("angle").degreesToRadians())}. * @@ -2434,8 +2497,7 @@ public static ASin asinOf(String fieldReference) { } /** - * Creates a new {@link AggregationExpression} that calculates the inverse sine of a value. - *
+ * Creates a new {@link AggregationExpression} that calculates the inverse sine of a value.
* * @param expression the {@link AggregationExpression expression} that resolves to a numeric value. * @return new instance of {@link ASin}. @@ -2484,8 +2546,7 @@ public static ASinh asinhOf(String fieldReference) { } /** - * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic sine of a value. - *
+ * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic sine of a value.
* * @param expression the {@link AggregationExpression expression} that resolves to a numeric value. * @return new instance of {@link ASinh}. @@ -2525,8 +2586,7 @@ private Cos(Object value) { /** * Creates a new {@link AggregationExpression} that calculates the cosine of a value that is measured in - * {@link AngularUnit#RADIANS radians}. - *
+ * {@link AngularUnit#RADIANS radians}.
* Use {@code cosOf("angle", DEGREES)} as shortcut for * *
@@ -2636,8 +2696,7 @@ public static Cosh coshOf(String fieldReference) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic cosine of a value that is measured in
-		 * the given {@link AngularUnit unit}.
-		 * 
+ * the given {@link AngularUnit unit}.
* Use {@code coshOf("angle", DEGREES)} as shortcut for * *
@@ -2654,8 +2713,7 @@ public static Cosh coshOf(String fieldReference, AngularUnit unit) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic cosine of a value that is measured in
-		 * {@link AngularUnit#RADIANS}.
-		 * 
+ * {@link AngularUnit#RADIANS}.
* Use {@code sinhOf("angle", DEGREES)} as shortcut for eg. * {@code sinhOf(ConvertOperators.valueOf("angle").degreesToRadians())}. * @@ -2738,8 +2796,7 @@ public static ACos acosOf(String fieldReference) { } /** - * Creates a new {@link AggregationExpression} that calculates the inverse cosine of a value. - *
+ * Creates a new {@link AggregationExpression} that calculates the inverse cosine of a value.
* * @param expression the {@link AggregationExpression expression} that resolves to a numeric value. * @return new instance of {@link ACos}. @@ -2788,8 +2845,7 @@ public static ACosh acoshOf(String fieldReference) { } /** - * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic cosine of a value. - *
+ * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic cosine of a value.
* * @param expression the {@link AggregationExpression expression} that resolves to a numeric value. * @return new instance of {@link ACosh}. @@ -2829,8 +2885,7 @@ private Tan(Object value) { /** * Creates a new {@link AggregationExpression} that calculates the tangent of a value that is measured in - * {@link AngularUnit#RADIANS radians}. - *
+ * {@link AngularUnit#RADIANS radians}.
* Use {@code tanOf("angle", DEGREES)} as shortcut for * *
@@ -3008,8 +3063,8 @@ public static ATan2 valueOf(AggregationExpression expression) {
 		 * Creates a new {@link AggregationExpression} that calculates the inverse tangent of of y / x, where y and x are
 		 * the first and second values passed to the expression respectively.
 		 *
-		 * @param fieldReference anything ({@link Field field}, {@link AggregationExpression expression}, ...) that resolves to a
-		 *          numeric value.
+		 * @param fieldReference anything ({@link Field field}, {@link AggregationExpression expression}, ...) that resolves
+		 *          to a numeric value.
 		 * @return new instance of {@link ATan2}.
 		 */
 		public ATan2 atan2of(String fieldReference) {
@@ -3022,8 +3077,8 @@ public ATan2 atan2of(String fieldReference) {
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic tangent of a value that is measured in
 		 * {@link AngularUnit#RADIANS}.
 		 *
-		 * @param expression anything ({@link Field field}, {@link AggregationExpression expression}, ...) that resolves to a
-		 *          numeric value.
+		 * @param expression anything ({@link Field field}, {@link AggregationExpression expression}, ...) that resolves to
+		 *          a numeric value.
 		 * @return new instance of {@link ATan2}.
 		 */
 		public ATan2 atan2of(AggregationExpression expression) {
@@ -3075,8 +3130,7 @@ public static Tanh tanhOf(String fieldReference) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic tangent of a value that is measured in
-		 * the given {@link AngularUnit unit}.
-		 * 
+ * the given {@link AngularUnit unit}.
* Use {@code tanhOf("angle", DEGREES)} as shortcut for * *
@@ -3093,8 +3147,7 @@ public static Tanh tanhOf(String fieldReference, AngularUnit unit) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic tangent of a value that is measured in
-		 * {@link AngularUnit#RADIANS}.
-		 * 
+ * {@link AngularUnit#RADIANS}.
* Use {@code sinhOf("angle", DEGREES)} as shortcut for eg. * {@code sinhOf(ConvertOperators.valueOf("angle").degreesToRadians())}. * @@ -3165,11 +3218,9 @@ private ATanh(Object value) { } /** - * Creates a new {@link AggregationExpression} that calculates the inverse - * hyperbolic tangent of a value. + * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic tangent of a value. * - * @param fieldReference the name of the {@link Field field} that resolves to a - * numeric value. + * @param fieldReference the name of the {@link Field field} that resolves to a numeric value. * @return new instance of {@link ATanh}. */ public static ATanh atanhOf(String fieldReference) { @@ -3177,8 +3228,7 @@ public static ATanh atanhOf(String fieldReference) { } /** - * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic tangent of a value. - *
+ * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic tangent of a value.
* * @param expression the {@link AggregationExpression expression} that resolves to a numeric value. * @return new instance of {@link ATanh}. @@ -3188,11 +3238,10 @@ public static ATanh atanhOf(AggregationExpression expression) { } /** - * Creates a new {@link AggregationExpression} that calculates the inverse - * hyperbolic tangent of a value. + * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic tangent of a value. * - * @param value anything ({@link Field field}, {@link AggregationExpression - * expression}, ...) that resolves to a numeric value. + * @param value anything ({@link Field field}, {@link AggregationExpression expression}, ...) that resolves to a + * numeric value. * @return new instance of {@link ATanh}. */ public static ATanh atanhOf(Object value) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java index 41688bfc62..85952d8f39 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java @@ -22,12 +22,14 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Range; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.aggregation.ArrayOperators.Filter.AsBuilder; import org.springframework.data.mongodb.core.aggregation.ArrayOperators.Reduce.PropertyExpression; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -158,6 +160,7 @@ public ArrayElemAt elementAt(String fieldReference) { return createArrayElemAt().elementAt(fieldReference); } + @SuppressWarnings("NullAway") private ArrayElemAt createArrayElemAt() { if (usesFieldRef()) { @@ -193,6 +196,7 @@ public ConcatArrays concat(AggregationExpression expression) { return createConcatArrays().concat(expression); } + @SuppressWarnings("NullAway") private ConcatArrays createConcatArrays() { if (usesFieldRef()) { @@ -208,6 +212,7 @@ private ConcatArrays createConcatArrays() { * * @return new instance of {@link AsBuilder} to create a {@link Filter}. */ + @SuppressWarnings("NullAway") public AsBuilder filter() { if (usesFieldRef()) { @@ -227,6 +232,7 @@ public AsBuilder filter() { * * @return new instance of {@link IsArray}. */ + @SuppressWarnings("NullAway") public IsArray isArray() { Assert.state(values == null, "Does it make sense to call isArray on an array; Maybe just skip it"); @@ -239,6 +245,7 @@ public IsArray isArray() { * * @return new instance of {@link Size}. */ + @SuppressWarnings("NullAway") public Size length() { if (usesFieldRef()) { @@ -253,6 +260,7 @@ public Size length() { * * @return new instance of {@link Slice}. */ + @SuppressWarnings("NullAway") public Slice slice() { if (usesFieldRef()) { @@ -269,6 +277,7 @@ public Slice slice() { * @param value must not be {@literal null}. * @return new instance of {@link IndexOfArray}. */ + @SuppressWarnings("NullAway") public IndexOfArray indexOf(Object value) { if (usesFieldRef()) { @@ -284,6 +293,7 @@ public IndexOfArray indexOf(Object value) { * * @return new instance of {@link ReverseArray}. */ + @SuppressWarnings("NullAway") public ReverseArray reverse() { if (usesFieldRef()) { @@ -301,6 +311,7 @@ public ReverseArray reverse() { * @param expression must not be {@literal null}. * @return new instance of {@link ReduceInitialValueBuilder} to create {@link Reduce}. */ + @SuppressWarnings("NullAway") public ArrayOperatorFactory.ReduceInitialValueBuilder reduce(AggregationExpression expression) { return initialValue -> (usesFieldRef() ? Reduce.arrayOf(fieldReference) @@ -314,6 +325,7 @@ public ArrayOperatorFactory.ReduceInitialValueBuilder reduce(AggregationExpressi * @param expressions must not be {@literal null}. * @return new instance of {@link ReduceInitialValueBuilder} to create {@link Reduce}. */ + @SuppressWarnings("NullAway") public ArrayOperatorFactory.ReduceInitialValueBuilder reduce(PropertyExpression... expressions) { return initialValue -> (usesFieldRef() ? Reduce.arrayOf(fieldReference) : Reduce.arrayOf(expression)) @@ -327,6 +339,7 @@ public ArrayOperatorFactory.ReduceInitialValueBuilder reduce(PropertyExpression. * @return new instance of {@link SortArray}. * @since 4.0 */ + @SuppressWarnings("NullAway") public SortArray sort(Sort sort) { if (usesFieldRef()) { @@ -336,6 +349,23 @@ public SortArray sort(Sort sort) { return (usesExpression() ? SortArray.sortArrayOf(expression) : SortArray.sortArray(values)).by(sort); } + /** + * Creates new {@link AggregationExpression} that takes the associated array and sorts it by the given + * {@link Direction order}. + * + * @return new instance of {@link SortArray}. + * @since 4.5 + */ + @SuppressWarnings("NullAway") + public SortArray sort(Direction direction) { + + if (usesFieldRef()) { + return SortArray.sortArrayOf(fieldReference).direction(direction); + } + + return (usesExpression() ? SortArray.sortArrayOf(expression) : SortArray.sortArray(values)).direction(direction); + } + /** * Creates new {@link AggregationExpression} that transposes an array of input arrays so that the first element of * the output array would be an array containing, the first element of the first input array, the first element of @@ -344,6 +374,7 @@ public SortArray sort(Sort sort) { * @param arrays must not be {@literal null}. * @return new instance of {@link Zip}. */ + @SuppressWarnings("NullAway") public Zip zipWith(Object... arrays) { if (usesFieldRef()) { @@ -360,6 +391,7 @@ public Zip zipWith(Object... arrays) { * @param value must not be {@literal null}. * @return new instance of {@link In}. */ + @SuppressWarnings("NullAway") public In containsValue(Object value) { if (usesFieldRef()) { @@ -376,6 +408,7 @@ public In containsValue(Object value) { * @return new instance of {@link ArrayToObject}. * @since 2.1 */ + @SuppressWarnings("NullAway") public ArrayToObject toObject() { if (usesFieldRef()) { @@ -392,6 +425,7 @@ public ArrayToObject toObject() { * @return new instance of {@link First}. * @since 3.4 */ + @SuppressWarnings("NullAway") public First first() { if (usesFieldRef()) { @@ -408,6 +442,7 @@ public First first() { * @return new instance of {@link Last}. * @since 3.4 */ + @SuppressWarnings("NullAway") public Last last() { if (usesFieldRef()) { @@ -506,6 +541,7 @@ public static ArrayElemAt arrayOf(Collection values) { * @param index the index number * @return new instance of {@link ArrayElemAt}. */ + @Contract("_ -> new") public ArrayElemAt elementAt(int index) { return new ArrayElemAt(append(index)); } @@ -516,6 +552,7 @@ public ArrayElemAt elementAt(int index) { * @param expression must not be {@literal null}. * @return new instance of {@link ArrayElemAt}. */ + @Contract("_ -> new") public ArrayElemAt elementAt(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -528,6 +565,7 @@ public ArrayElemAt elementAt(AggregationExpression expression) { * @param arrayFieldReference the field name. * @return new instance of {@link ArrayElemAt}. */ + @Contract("_ -> new") public ArrayElemAt elementAt(String arrayFieldReference) { Assert.notNull(arrayFieldReference, "ArrayReference must not be null"); @@ -594,6 +632,7 @@ public static ConcatArrays arrayOf(Collection values) { * @param arrayFieldReference must not be {@literal null}. * @return new instance of {@link ConcatArrays}. */ + @Contract("_ -> new") public ConcatArrays concat(String arrayFieldReference) { Assert.notNull(arrayFieldReference, "ArrayFieldReference must not be null"); @@ -606,6 +645,7 @@ public ConcatArrays concat(String arrayFieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link ConcatArrays}. */ + @Contract("_ -> new") public ConcatArrays concat(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -681,9 +721,12 @@ public static AsBuilder filter(List values) { @Override public Document toDocument(final AggregationOperationContext context) { + + Assert.notNull(as, "As must be set first"); return toFilter(ExposedFields.from(as), context); } + @SuppressWarnings("NullAway") private Document toFilter(ExposedFields exposedFields, AggregationOperationContext context) { Document filterExpression = new Document(); @@ -697,7 +740,7 @@ private Document toFilter(ExposedFields exposedFields, AggregationOperationConte return new Document("$filter", filterExpression); } - private Object getMappedInput(AggregationOperationContext context) { + private @Nullable Object getMappedInput(AggregationOperationContext context) { if (input instanceof Field field) { return context.getReference(field).toString(); @@ -710,7 +753,7 @@ private Object getMappedInput(AggregationOperationContext context) { return input; } - private Object getMappedCondition(AggregationOperationContext context) { + private @Nullable Object getMappedCondition(AggregationOperationContext context) { if (!(condition instanceof AggregationExpression aggregationExpression)) { return condition; @@ -817,6 +860,7 @@ public static InputBuilder newBuilder() { } @Override + @Contract("_ -> this") public AsBuilder filter(List array) { Assert.notNull(array, "Array must not be null"); @@ -825,6 +869,7 @@ public AsBuilder filter(List array) { } @Override + @Contract("_ -> this") public AsBuilder filter(Field field) { Assert.notNull(field, "Field must not be null"); @@ -833,6 +878,7 @@ public AsBuilder filter(Field field) { } @Override + @Contract("_ -> this") public AsBuilder filter(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -841,6 +887,7 @@ public AsBuilder filter(AggregationExpression expression) { } @Override + @Contract("_ -> this") public ConditionBuilder as(String variableName) { Assert.notNull(variableName, "Variable name must not be null"); @@ -1028,6 +1075,7 @@ public static Slice sliceArrayOf(Collection values) { * @param count number of elements to slice. * @return new instance of {@link Slice}. */ + @Contract("_ -> new") public Slice itemCount(int count) { return new Slice(append(count)); } @@ -1040,6 +1088,7 @@ public Slice itemCount(int count) { * @return new instance of {@link Slice}. * @since 4.5 */ + @Contract("_ -> new") public Slice itemCount(AggregationExpression count) { return new Slice(append(count)); } @@ -1158,6 +1207,7 @@ public static IndexOfArrayBuilder arrayOf(Collection values) { * @param range the lookup range. * @return new instance of {@link IndexOfArray}. */ + @Contract("_ -> new") public IndexOfArray within(Range range) { return new IndexOfArray(append(AggregationUtils.toRangeValues(range))); } @@ -1233,6 +1283,7 @@ public static RangeOperatorBuilder rangeStartingAt(long value) { return new RangeOperatorBuilder(value); } + @Contract("_ -> new") public RangeOperator withStepSize(long stepSize) { return new RangeOperator(append(stepSize)); } @@ -1365,6 +1416,7 @@ public Document toDocument(AggregationOperationContext context) { return new Document("$reduce", document); } + @SuppressWarnings("NullAway") private Object getMappedValue(Object value, AggregationOperationContext context) { if (value instanceof Document) { @@ -1684,6 +1736,7 @@ public static ZipBuilder arrayOf(Collection values) { * * @return new instance of {@link Zip}. */ + @Contract("-> new") public Zip useLongestLength() { return new Zip(append("useLongestLength", true)); } @@ -1694,6 +1747,7 @@ public Zip useLongestLength() { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Zip}. */ + @Contract("_ -> new") public Zip defaultTo(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1706,6 +1760,7 @@ public Zip defaultTo(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Zip}. */ + @Contract("_ -> new") public Zip defaultTo(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1718,6 +1773,7 @@ public Zip defaultTo(AggregationExpression expression) { * @param array must not be {@literal null}. * @return new instance of {@link Zip}. */ + @Contract("_ -> new") public Zip defaultTo(Object[] array) { Assert.notNull(array, "Array must not be null"); @@ -1943,10 +1999,6 @@ public static First firstOf(AggregationExpression expression) { return new First(expression); } - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregationExpression#getMongoMethod() - */ @Override protected String getMongoMethod() { return "$first"; @@ -1997,10 +2049,6 @@ public static Last lastOf(AggregationExpression expression) { return new Last(expression); } - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregationExpression#getMongoMethod() - */ @Override protected String getMongoMethod() { return "$last"; @@ -2055,14 +2103,44 @@ public static SortArray sortArrayOf(AggregationExpression expression) { * @param sort must not be {@literal null}. * @return new instance of {@link SortArray}. */ + @Contract("_ -> new") public SortArray by(Sort sort) { return new SortArray(append("sortBy", sort)); } - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregationExpression#getMongoMethod() + /** + * Order the values for the array in the given direction. + * + * @param direction must not be {@literal null}. + * @return new instance of {@link SortArray}. + * @since 4.5 + */ + public SortArray direction(Direction direction) { + return new SortArray(append("sortBy", direction.isAscending() ? 1 : -1)); + } + + /** + * Sort the array elements by their values in ascending order. Suitable for arrays of simple types (e.g., integers, + * strings). + * + * @return new instance of {@link SortArray}. + * @since 4.5 */ + public SortArray byValueAscending() { + return direction(Direction.ASC); + } + + /** + * Sort the array elements by their values in descending order. Suitable for arrays of simple types (e.g., integers, + * strings). + * + * @return new instance of {@link SortArray}. + * @since 4.5 + */ + public SortArray byValueDescending() { + return direction(Direction.DESC); + } + @Override protected String getMongoMethod() { return "$sortArray"; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BooleanOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BooleanOperators.java index 69689908c9..f3ffdb7ad1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BooleanOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BooleanOperators.java @@ -19,6 +19,8 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -77,8 +79,8 @@ public static Not not(AggregationExpression expression) { */ public static class BooleanOperatorFactory { - private final String fieldReference; - private final AggregationExpression expression; + private final @Nullable String fieldReference; + private final @Nullable AggregationExpression expression; /** * Creates new {@link BooleanOperatorFactory} for given {@literal fieldReference}. @@ -130,6 +132,7 @@ public And and(String fieldReference) { return createAnd().andField(fieldReference); } + @SuppressWarnings("NullAway") private And createAnd() { return usesFieldRef() ? And.and(Fields.field(fieldReference)) : And.and(expression); } @@ -160,6 +163,7 @@ public Or or(String fieldReference) { return createOr().orField(fieldReference); } + @SuppressWarnings("NullAway") private Or createOr() { return usesFieldRef() ? Or.or(Fields.field(fieldReference)) : Or.or(expression); } @@ -169,6 +173,7 @@ private Or createOr() { * * @return new instance of {@link Not}. */ + @SuppressWarnings("NullAway") public Not not() { return usesFieldRef() ? Not.not(fieldReference) : Not.not(expression); } @@ -211,6 +216,7 @@ public static And and(Object... expressions) { * @param expression must not be {@literal null}. * @return new instance of {@link And}. */ + @Contract("_ -> new") public And andExpression(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -223,6 +229,7 @@ public And andExpression(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link And}. */ + @Contract("_ -> new") public And andField(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -235,6 +242,7 @@ public And andField(String fieldReference) { * @param value must not be {@literal null}. * @return new instance of {@link And}. */ + @Contract("_ -> new") public And andValue(Object value) { Assert.notNull(value, "Value must not be null"); @@ -277,6 +285,7 @@ public static Or or(Object... expressions) { * @param expression must not be {@literal null}. * @return new instance of {@link Or}. */ + @Contract("_ -> new") public Or orExpression(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -289,6 +298,7 @@ public Or orExpression(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Or}. */ + @Contract("_ -> new") public Or orField(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -301,6 +311,7 @@ public Or orField(String fieldReference) { * @param value must not be {@literal null}. * @return new instance of {@link Or}. */ + @Contract("_ -> new") public Or orValue(Object value) { Assert.notNull(value, "Value must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketAutoOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketAutoOperation.java index 36492e2a81..16eca4ec22 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketAutoOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketAutoOperation.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core.aggregation; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.BucketAutoOperation.BucketAutoOperationOutputBuilder; import org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.OutputBuilder; import org.springframework.util.Assert; @@ -38,7 +39,7 @@ public class BucketAutoOperation extends BucketOperationSupport boundaries; - private final Object defaultBucket; + private final @Nullable Object defaultBucket; /** * Creates a new {@link BucketOperation} given a {@link Field group-by field}. @@ -76,7 +78,7 @@ private BucketOperation(BucketOperation bucketOperation, Outputs outputs) { this.defaultBucket = bucketOperation.defaultBucket; } - private BucketOperation(BucketOperation bucketOperation, List boundaries, Object defaultBucket) { + private BucketOperation(BucketOperation bucketOperation, List boundaries, @Nullable Object defaultBucket) { super(bucketOperation); @@ -111,6 +113,7 @@ public String getOperator() { * @param literal must not be {@literal null}. * @return new instance of {@link BucketOperation}. */ + @Contract("_ -> new") public BucketOperation withDefaultBucket(Object literal) { Assert.notNull(literal, "Default bucket literal must not be null"); @@ -124,6 +127,7 @@ public BucketOperation withDefaultBucket(Object literal) { * @param boundaries must not be {@literal null}. * @return new instance of {@link BucketOperation}. */ + @Contract("_ -> new") public BucketOperation withBoundaries(Object... boundaries) { Assert.notNull(boundaries, "Boundaries must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperationSupport.java index e19ad59a3f..3d5ded05c2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperationSupport.java @@ -22,6 +22,7 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.OutputBuilder; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder; @@ -40,8 +41,8 @@ public abstract class BucketOperationSupport, B extends OutputBuilder> implements FieldsExposingAggregationOperation { - private final Field groupByField; - private final AggregationExpression groupByExpression; + private final @Nullable Field groupByField; + private final @Nullable AggregationExpression groupByExpression; private final Outputs outputs; /** @@ -142,12 +143,17 @@ public Document toDocument(AggregationOperationContext context) { } @Override + public Document toDocument(AggregationOperationContext context) { Document document = new Document(); - document.put("groupBy", groupByExpression == null ? context.getReference(groupByField).toString() - : groupByExpression.toDocument(context)); + if(groupByExpression != null) { + document.put("groupBy", groupByExpression.toDocument(context)); + } else if (groupByField != null) { + document.put("groupBy", context.getReference(groupByField).toString()); + + } if (!outputs.isEmpty()) { document.put("output", outputs.toDocument(context)); @@ -625,7 +631,9 @@ public SpelExpressionOutput(String expression, Object[] parameters) { @Override public Document toDocument(AggregationOperationContext context) { - return (Document) TRANSFORMER.transform(expression, context, params); + + Object o = TRANSFORMER.transform(expression, context, params); + return o instanceof Document document ? document : new Document(); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ComparisonOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ComparisonOperators.java index f27b7f16cb..e2626c3a16 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ComparisonOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ComparisonOperators.java @@ -18,6 +18,8 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -50,8 +52,8 @@ public static ComparisonOperatorFactory valueOf(AggregationExpression expression public static class ComparisonOperatorFactory { - private final String fieldReference; - private final AggregationExpression expression; + private final @Nullable String fieldReference; + private final @Nullable AggregationExpression expression; /** * Creates new {@link ComparisonOperatorFactory} for given {@literal fieldReference}. @@ -107,6 +109,7 @@ public Cmp compareToValue(Object value) { return createCmp().compareToValue(value); } + @SuppressWarnings("NullAway") private Cmp createCmp() { return usesFieldRef() ? Cmp.valueOf(fieldReference) : Cmp.valueOf(expression); } @@ -144,6 +147,7 @@ public Eq equalToValue(Object value) { return createEq().equalToValue(value); } + @SuppressWarnings("NullAway") private Eq createEq() { return usesFieldRef() ? Eq.valueOf(fieldReference) : Eq.valueOf(expression); } @@ -181,6 +185,7 @@ public Gt greaterThanValue(Object value) { return createGt().greaterThanValue(value); } + @SuppressWarnings("NullAway") private Gt createGt() { return usesFieldRef() ? Gt.valueOf(fieldReference) : Gt.valueOf(expression); } @@ -218,6 +223,7 @@ public Gte greaterThanEqualToValue(Object value) { return createGte().greaterThanEqualToValue(value); } + @SuppressWarnings("NullAway") private Gte createGte() { return usesFieldRef() ? Gte.valueOf(fieldReference) : Gte.valueOf(expression); } @@ -255,6 +261,7 @@ public Lt lessThanValue(Object value) { return createLt().lessThanValue(value); } + @SuppressWarnings("NullAway") private Lt createLt() { return usesFieldRef() ? Lt.valueOf(fieldReference) : Lt.valueOf(expression); } @@ -292,6 +299,7 @@ public Lte lessThanEqualToValue(Object value) { return createLte().lessThanEqualToValue(value); } + @SuppressWarnings("NullAway") private Lte createLte() { return usesFieldRef() ? Lte.valueOf(fieldReference) : Lte.valueOf(expression); } @@ -329,6 +337,7 @@ public Ne notEqualToValue(Object value) { return createNe().notEqualToValue(value); } + @SuppressWarnings("NullAway") private Ne createNe() { return usesFieldRef() ? Ne.valueOf(fieldReference) : Ne.valueOf(expression); } @@ -384,6 +393,7 @@ public static Cmp valueOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Cmp}. */ + @Contract("_ -> new") public Cmp compareTo(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -396,6 +406,7 @@ public Cmp compareTo(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Cmp}. */ + @Contract("_ -> new") public Cmp compareTo(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -408,6 +419,7 @@ public Cmp compareTo(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Cmp}. */ + @Contract("_ -> new") public Cmp compareToValue(Object value) { Assert.notNull(value, "Value must not be null"); @@ -461,6 +473,7 @@ public static Eq valueOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Eq}. */ + @Contract("_ -> new") public Eq equalTo(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -473,6 +486,7 @@ public Eq equalTo(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Eq}. */ + @Contract("_ -> new") public Eq equalTo(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -485,6 +499,7 @@ public Eq equalTo(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Eq}. */ + @Contract("_ -> new") public Eq equalToValue(Object value) { Assert.notNull(value, "Value must not be null"); @@ -538,6 +553,7 @@ public static Gt valueOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Gt}. */ + @Contract("_ -> new") public Gt greaterThan(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -550,6 +566,7 @@ public Gt greaterThan(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Gt}. */ + @Contract("_ -> new") public Gt greaterThan(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -562,6 +579,7 @@ public Gt greaterThan(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Gt}. */ + @Contract("_ -> new") public Gt greaterThanValue(Object value) { Assert.notNull(value, "Value must not be null"); @@ -615,6 +633,7 @@ public static Lt valueOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Lt}. */ + @Contract("_ -> new") public Lt lessThan(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -627,6 +646,7 @@ public Lt lessThan(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Lt}. */ + @Contract("_ -> new") public Lt lessThan(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -639,6 +659,7 @@ public Lt lessThan(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Lt}. */ + @Contract("_ -> new") public Lt lessThanValue(Object value) { Assert.notNull(value, "Value must not be null"); @@ -692,6 +713,7 @@ public static Gte valueOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Gte}. */ + @Contract("_ -> new") public Gte greaterThanEqualTo(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -704,6 +726,7 @@ public Gte greaterThanEqualTo(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Gte}. */ + @Contract("_ -> new") public Gte greaterThanEqualTo(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -716,6 +739,7 @@ public Gte greaterThanEqualTo(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Gte}. */ + @Contract("_ -> new") public Gte greaterThanEqualToValue(Object value) { Assert.notNull(value, "Value must not be null"); @@ -769,6 +793,7 @@ public static Lte valueOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Lte}. */ + @Contract("_ -> new") public Lte lessThanEqualTo(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -781,6 +806,7 @@ public Lte lessThanEqualTo(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Lte}. */ + @Contract("_ -> new") public Lte lessThanEqualTo(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -793,6 +819,7 @@ public Lte lessThanEqualTo(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Lte}. */ + @Contract("_ -> new") public Lte lessThanEqualToValue(Object value) { Assert.notNull(value, "Value must not be null"); @@ -846,6 +873,7 @@ public static Ne valueOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Ne}. */ + @Contract("_ -> new") public Ne notEqualTo(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -858,6 +886,7 @@ public Ne notEqualTo(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Ne}. */ + @Contract("_ -> new") public Ne notEqualTo(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -870,6 +899,7 @@ public Ne notEqualTo(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Ne}. */ + @Contract("_ -> new") public Ne notEqualToValue(Object value) { Assert.notNull(value, "Value must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperators.java index 323a11895b..462d94d6f1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperators.java @@ -22,12 +22,13 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Cond.OtherwiseBuilder; import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Cond.ThenBuilder; import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Switch.CaseOperator; import org.springframework.data.mongodb.core.query.CriteriaDefinition; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -211,6 +212,7 @@ public OtherwiseBuilder thenValueOf(String fieldReference) { return createThenBuilder().thenValueOf(fieldReference); } + @SuppressWarnings("NullAway") private ThenBuilder createThenBuilder() { if (usesFieldRef()) { @@ -303,6 +305,7 @@ private Object mapCondition(Object condition, AggregationOperationContext contex } } + @SuppressWarnings("NullAway") private Object resolve(Object value, AggregationOperationContext context) { if (value instanceof Field field) { @@ -389,7 +392,7 @@ public interface ThenBuilder extends OrBuilder { */ static final class IfNullOperatorBuilder implements IfNullBuilder, ThenBuilder { - private @Nullable List conditions; + private List conditions; private IfNullOperatorBuilder() { conditions = new ArrayList<>(); @@ -404,6 +407,7 @@ public static IfNullOperatorBuilder newBuilder() { return new IfNullOperatorBuilder(); } + @Contract("_ -> this") public ThenBuilder ifNull(String fieldReference) { Assert.hasText(fieldReference, "FieldReference name must not be null or empty"); @@ -412,6 +416,7 @@ public ThenBuilder ifNull(String fieldReference) { } @Override + @Contract("_ -> this") public ThenBuilder ifNull(AggregationExpression expression) { Assert.notNull(expression, "AggregationExpression name must not be null or empty"); @@ -420,25 +425,30 @@ public ThenBuilder ifNull(AggregationExpression expression) { } @Override + @Contract("_ -> this") public ThenBuilder orIfNull(String fieldReference) { return ifNull(fieldReference); } @Override + @Contract("_ -> this") public ThenBuilder orIfNull(AggregationExpression expression) { return ifNull(expression); } + @Contract("_ -> new") public IfNull then(Object value) { return new IfNull(conditions, value); } + @Contract("_ -> new") public IfNull thenValueOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); return new IfNull(conditions, Fields.field(fieldReference)); } + @Contract("_ -> new") public IfNull thenValueOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -491,6 +501,7 @@ public static Switch switchCases(List conditions) { * @param value must not be {@literal null}. * @return new instance of {@link Switch}. */ + @Contract("_ -> new") public Switch defaultTo(Object value) { return new Switch(append("default", value)); } @@ -623,6 +634,7 @@ public Document toDocument(AggregationOperationContext context) { return new Document("$cond", condObject); } + @SuppressWarnings("NullAway") private Object resolveValue(AggregationOperationContext context, Object value) { if (value instanceof Document || value instanceof Field) { @@ -886,6 +898,7 @@ public static ConditionalExpressionBuilder newBuilder() { } @Override + @Contract("_ -> this") public ConditionalExpressionBuilder when(Document booleanExpression) { Assert.notNull(booleanExpression, "'Boolean expression' must not be null"); @@ -895,6 +908,7 @@ public ConditionalExpressionBuilder when(Document booleanExpression) { } @Override + @Contract("_ -> this") public ThenBuilder when(CriteriaDefinition criteria) { Assert.notNull(criteria, "Criteria must not be null"); @@ -903,6 +917,7 @@ public ThenBuilder when(CriteriaDefinition criteria) { } @Override + @Contract("_ -> this") public ThenBuilder when(AggregationExpression expression) { Assert.notNull(expression, "AggregationExpression field must not be null"); @@ -911,6 +926,7 @@ public ThenBuilder when(AggregationExpression expression) { } @Override + @Contract("_ -> this") public ThenBuilder when(String booleanField) { Assert.hasText(booleanField, "Boolean field name must not be null or empty"); @@ -919,6 +935,7 @@ public ThenBuilder when(String booleanField) { } @Override + @Contract("_ -> this") public OtherwiseBuilder then(Object thenValue) { Assert.notNull(thenValue, "Then-value must not be null"); @@ -927,6 +944,7 @@ public OtherwiseBuilder then(Object thenValue) { } @Override + @Contract("_ -> this") public OtherwiseBuilder thenValueOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -935,6 +953,7 @@ public OtherwiseBuilder thenValueOf(String fieldReference) { } @Override + @Contract("_ -> this") public OtherwiseBuilder thenValueOf(AggregationExpression expression) { Assert.notNull(expression, "AggregationExpression must not be null"); @@ -943,23 +962,32 @@ public OtherwiseBuilder thenValueOf(AggregationExpression expression) { } @Override + @Contract("_ -> new") public Cond otherwise(Object otherwiseValue) { Assert.notNull(otherwiseValue, "Value must not be null"); + Assert.notNull(condition, "Condition value needs to be set first"); + Assert.notNull(thenValue, "Then value needs to be set first"); return new Cond(condition, thenValue, otherwiseValue); } @Override + @Contract("_ -> new") public Cond otherwiseValueOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); + Assert.notNull(condition, "Condition value needs to be set first"); + Assert.notNull(thenValue, "Then value needs to be set first"); return new Cond(condition, thenValue, Fields.field(fieldReference)); } @Override + @Contract("_ -> new") public Cond otherwiseValueOf(AggregationExpression expression) { Assert.notNull(expression, "AggregationExpression must not be null"); + Assert.notNull(condition, "Condition value needs to be set first"); + Assert.notNull(thenValue, "Then value needs to be set first"); return new Cond(condition, thenValue, expression); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConvertOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConvertOperators.java index aa085b2a29..35a6ad061c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConvertOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConvertOperators.java @@ -17,8 +17,8 @@ import java.util.Collections; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -242,10 +242,12 @@ public DegreesToRadians convertDegreesToRadians() { return DegreesToRadians.degreesToRadians(valueObject()); } + @SuppressWarnings("NullAway") private Convert createConvert() { return usesFieldRef() ? Convert.convertValueOf(fieldReference) : Convert.convertValueOf(expression); } + @SuppressWarnings("NullAway") private Object valueObject() { return usesFieldRef() ? Fields.field(fieldReference) : expression; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java index ff6ed7e983..7bf8a231ff 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java @@ -26,7 +26,8 @@ import java.util.TimeZone; import java.util.concurrent.TimeUnit; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -848,6 +849,7 @@ public TsSecond tsSecond() { return TsSecond.tsSecond(dateReference()); } + @SuppressWarnings("NullAway") private Object dateReference() { if (usesFieldRef()) { @@ -1076,6 +1078,7 @@ public static DayOfYear dayOfYear(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public DayOfYear withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1148,6 +1151,7 @@ public static DayOfMonth dayOfMonth(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public DayOfMonth withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1220,6 +1224,7 @@ public static DayOfWeek dayOfWeek(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public DayOfWeek withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1292,6 +1297,7 @@ public static Year yearOf(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public Year withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1364,6 +1370,7 @@ public static Month monthOf(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public Month withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1436,6 +1443,7 @@ public static Week weekOf(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public Week withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1508,6 +1516,7 @@ public static Hour hourOf(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public Hour withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1580,6 +1589,7 @@ public static Minute minuteOf(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public Minute withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1652,6 +1662,7 @@ public static Second secondOf(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public Second withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1724,6 +1735,7 @@ public static Millisecond millisecondOf(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public Millisecond withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1810,6 +1822,7 @@ public static FormatBuilder dateOf(final AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public DateToString withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1824,6 +1837,7 @@ public DateToString withTimezone(Timezone timezone) { * @return new instance of {@link DateToString}. * @since 2.1 */ + @Contract("_ -> new") public DateToString onNullReturn(Object value) { return new DateToString(append("onNull", value)); } @@ -1836,6 +1850,7 @@ public DateToString onNullReturn(Object value) { * @return new instance of {@link DateToString}. * @since 2.1 */ + @Contract("_ -> new") public DateToString onNullReturnValueOf(String fieldReference) { return onNullReturn(Fields.field(fieldReference)); } @@ -1848,6 +1863,7 @@ public DateToString onNullReturnValueOf(String fieldReference) { * @return new instance of {@link DateToString}. * @since 2.1 */ + @Contract("_ -> new") public DateToString onNullReturnValueOf(AggregationExpression expression) { return onNullReturn(expression); } @@ -1973,6 +1989,7 @@ public static IsoDayOfWeek isoDayOfWeek(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public IsoDayOfWeek withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -2045,6 +2062,7 @@ public static IsoWeek isoWeekOf(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public IsoWeek withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -2117,6 +2135,7 @@ public static IsoWeekYear isoWeekYearOf(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public IsoWeekYear withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -2301,6 +2320,7 @@ public static DateFromPartsWithYear dateFromParts() { * @return new instance. * @throws IllegalArgumentException if given {@literal month} is {@literal null}. */ + @Contract("_ -> new") public DateFromParts month(Object month) { return new DateFromParts(append("month", month)); } @@ -2312,6 +2332,7 @@ public DateFromParts month(Object month) { * @return new instance. * @throws IllegalArgumentException if given {@literal fieldReference} is {@literal null}. */ + @Contract("_ -> new") public DateFromParts monthOf(String fieldReference) { return month(Fields.field(fieldReference)); } @@ -2323,6 +2344,7 @@ public DateFromParts monthOf(String fieldReference) { * @return new instance. * @throws IllegalArgumentException if given {@literal expression} is {@literal null}. */ + @Contract("_ -> new") public DateFromParts monthOf(AggregationExpression expression) { return month(expression); } @@ -2335,6 +2357,7 @@ public DateFromParts monthOf(AggregationExpression expression) { * @return new instance. * @throws IllegalArgumentException if given {@literal day} is {@literal null}. */ + @Contract("_ -> new") public DateFromParts day(Object day) { return new DateFromParts(append("day", day)); } @@ -2346,6 +2369,7 @@ public DateFromParts day(Object day) { * @return new instance. * @throws IllegalArgumentException if given {@literal fieldReference} is {@literal null}. */ + @Contract("_ -> new") public DateFromParts dayOf(String fieldReference) { return day(Fields.field(fieldReference)); } @@ -2357,26 +2381,31 @@ public DateFromParts dayOf(String fieldReference) { * @return new instance. * @throws IllegalArgumentException if given {@literal expression} is {@literal null}. */ + @Contract("_ -> new") public DateFromParts dayOf(AggregationExpression expression) { return day(expression); } @Override + @Contract("_ -> new") public DateFromParts hour(Object hour) { return new DateFromParts(append("hour", hour)); } @Override + @Contract("_ -> new") public DateFromParts minute(Object minute) { return new DateFromParts(append("minute", minute)); } @Override + @Contract("_ -> new") public DateFromParts second(Object second) { return new DateFromParts(append("second", second)); } @Override + @Contract("_ -> new") public DateFromParts millisecond(Object millisecond) { return new DateFromParts(append("millisecond", millisecond)); } @@ -2390,6 +2419,7 @@ public DateFromParts millisecond(Object millisecond) { * @throws IllegalArgumentException if given {@literal timezone} is {@literal null}. */ @Override + @Contract("_ -> new") public DateFromParts withTimezone(Timezone timezone) { return new DateFromParts(appendTimezone(argumentMap(), timezone)); } @@ -2477,6 +2507,7 @@ public static IsoDateFromPartsWithYear dateFromParts() { * @return new instance. * @throws IllegalArgumentException if given {@literal isoWeek} is {@literal null}. */ + @Contract("_ -> new") public IsoDateFromParts isoWeek(Object isoWeek) { return new IsoDateFromParts(append("isoWeek", isoWeek)); } @@ -2488,6 +2519,7 @@ public IsoDateFromParts isoWeek(Object isoWeek) { * @return new instance. * @throws IllegalArgumentException if given {@literal fieldReference} is {@literal null}. */ + @Contract("_ -> new") public IsoDateFromParts isoWeekOf(String fieldReference) { return isoWeek(Fields.field(fieldReference)); } @@ -2499,6 +2531,7 @@ public IsoDateFromParts isoWeekOf(String fieldReference) { * @return new instance. * @throws IllegalArgumentException if given {@literal expression} is {@literal null}. */ + @Contract("_ -> new") public IsoDateFromParts isoWeekOf(AggregationExpression expression) { return isoWeek(expression); } @@ -2511,6 +2544,7 @@ public IsoDateFromParts isoWeekOf(AggregationExpression expression) { * @return new instance. * @throws IllegalArgumentException if given {@literal isoWeek} is {@literal null}. */ + @Contract("_ -> new") public IsoDateFromParts isoDayOfWeek(Object day) { return new IsoDateFromParts(append("isoDayOfWeek", day)); } @@ -2522,6 +2556,7 @@ public IsoDateFromParts isoDayOfWeek(Object day) { * @return new instance. * @throws IllegalArgumentException if given {@literal fieldReference} is {@literal null}. */ + @Contract("_ -> new") public IsoDateFromParts isoDayOfWeekOf(String fieldReference) { return isoDayOfWeek(Fields.field(fieldReference)); } @@ -2533,26 +2568,31 @@ public IsoDateFromParts isoDayOfWeekOf(String fieldReference) { * @return new instance. * @throws IllegalArgumentException if given {@literal expression} is {@literal null}. */ + @Contract("_ -> new") public IsoDateFromParts isoDayOfWeekOf(AggregationExpression expression) { return isoDayOfWeek(expression); } @Override + @Contract("_ -> new") public IsoDateFromParts hour(Object hour) { return new IsoDateFromParts(append("hour", hour)); } @Override + @Contract("_ -> new") public IsoDateFromParts minute(Object minute) { return new IsoDateFromParts(append("minute", minute)); } @Override + @Contract("_ -> new") public IsoDateFromParts second(Object second) { return new IsoDateFromParts(append("second", second)); } @Override + @Contract("_ -> new") public IsoDateFromParts millisecond(Object millisecond) { return new IsoDateFromParts(append("millisecond", millisecond)); } @@ -2566,6 +2606,7 @@ public IsoDateFromParts millisecond(Object millisecond) { * @throws IllegalArgumentException if given {@literal timezone} is {@literal null}. */ @Override + @Contract("_ -> new") public IsoDateFromParts withTimezone(Timezone timezone) { return new IsoDateFromParts(appendTimezone(argumentMap(), timezone)); } @@ -2676,6 +2717,7 @@ public static DateToParts datePartsOf(AggregationExpression expression) { * * @return new instance of {@link DateToParts}. */ + @Contract("_ -> new") public DateToParts iso8601() { return new DateToParts(append("iso8601", true)); } @@ -2689,6 +2731,7 @@ public DateToParts iso8601() { * @throws IllegalArgumentException if given {@literal timezone} is {@literal null}. */ @Override + @Contract("_ -> new") public DateToParts withTimezone(Timezone timezone) { return new DateToParts(appendTimezone(argumentMap(), timezone)); } @@ -2733,6 +2776,7 @@ public static DateFromString fromString(Object value) { * @return new instance of {@link DateFromString}. * @throws IllegalArgumentException if given {@literal fieldReference} is {@literal null}. */ + @Contract("_ -> new") public static DateFromString fromStringOf(String fieldReference) { return fromString(Fields.field(fieldReference)); } @@ -2744,6 +2788,7 @@ public static DateFromString fromStringOf(String fieldReference) { * @return new instance of {@link DateFromString}. * @throws IllegalArgumentException if given {@literal expression} is {@literal null}. */ + @Contract("_ -> new") public static DateFromString fromStringOf(AggregationExpression expression) { return fromString(expression); } @@ -2757,6 +2802,7 @@ public static DateFromString fromStringOf(AggregationExpression expression) { * @throws IllegalArgumentException if given {@literal timezone} is {@literal null}. */ @Override + @Contract("_ -> new") public DateFromString withTimezone(Timezone timezone) { return new DateFromString(appendTimezone(argumentMap(), timezone)); } @@ -2769,6 +2815,7 @@ public DateFromString withTimezone(Timezone timezone) { * @return new instance of {@link DateFromString}. * @throws IllegalArgumentException if given {@literal format} is {@literal null}. */ + @Contract("_ -> new") public DateFromString withFormat(String format) { Assert.notNull(format, "Format must not be null"); @@ -2838,6 +2885,7 @@ public static DateAdd addValue(Object value, String unit) { * @param expression must not be {@literal null}. * @return new instance of {@link DateAdd}. */ + @Contract("_ -> new") public DateAdd toDateOf(AggregationExpression expression) { return toDate(expression); } @@ -2848,6 +2896,7 @@ public DateAdd toDateOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link DateAdd}. */ + @Contract("_ -> new") public DateAdd toDateOf(String fieldReference) { return toDate(Fields.field(fieldReference)); } @@ -2858,6 +2907,7 @@ public DateAdd toDateOf(String fieldReference) { * @param dateExpression anything that evaluates to a valid date. Must not be {@literal null}. * @return new instance of {@link DateAdd}. */ + @Contract("_ -> new") public DateAdd toDate(Object dateExpression) { return new DateAdd(append("startDate", dateExpression)); } @@ -2868,6 +2918,7 @@ public DateAdd toDate(Object dateExpression) { * @param timezone must not be {@literal null}. Consider {@link Timezone#none()} instead. * @return new instance of {@link DateAdd}. */ + @Contract("_ -> new") public DateAdd withTimezone(Timezone timezone) { return new DateAdd(appendTimezone(argumentMap(), timezone)); } @@ -2935,6 +2986,7 @@ public static DateSubtract subtractValue(Object value, String unit) { * @param expression must not be {@literal null}. * @return new instance of {@link DateSubtract}. */ + @Contract("_ -> new") public DateSubtract fromDateOf(AggregationExpression expression) { return fromDate(expression); } @@ -2945,6 +2997,7 @@ public DateSubtract fromDateOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link DateSubtract}. */ + @Contract("_ -> new") public DateSubtract fromDateOf(String fieldReference) { return fromDate(Fields.field(fieldReference)); } @@ -2955,6 +3008,7 @@ public DateSubtract fromDateOf(String fieldReference) { * @param dateExpression anything that evaluates to a valid date. Must not be {@literal null}. * @return new instance of {@link DateSubtract}. */ + @Contract("_ -> new") public DateSubtract fromDate(Object dateExpression) { return new DateSubtract(append("startDate", dateExpression)); } @@ -2965,6 +3019,7 @@ public DateSubtract fromDate(Object dateExpression) { * @param timezone must not be {@literal null}. Consider {@link Timezone#none()} instead. * @return new instance of {@link DateSubtract}. */ + @Contract("_ -> new") public DateSubtract withTimezone(Timezone timezone) { return new DateSubtract(appendTimezone(argumentMap(), timezone)); } @@ -3032,6 +3087,7 @@ public static DateDiff diffValue(Object value, String unit) { * @param expression must not be {@literal null}. * @return new instance of {@link DateAdd}. */ + @Contract("_ -> new") public DateDiff toDateOf(AggregationExpression expression) { return toDate(expression); } @@ -3042,6 +3098,7 @@ public DateDiff toDateOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link DateAdd}. */ + @Contract("_ -> new") public DateDiff toDateOf(String fieldReference) { return toDate(Fields.field(fieldReference)); } @@ -3052,6 +3109,7 @@ public DateDiff toDateOf(String fieldReference) { * @param dateExpression anything that evaluates to a valid date. Must not be {@literal null}. * @return new instance of {@link DateAdd}. */ + @Contract("_ -> new") public DateDiff toDate(Object dateExpression) { return new DateDiff(append("startDate", dateExpression)); } @@ -3062,6 +3120,7 @@ public DateDiff toDate(Object dateExpression) { * @param timezone must not be {@literal null}. Consider {@link Timezone#none()} instead. * @return new instance of {@link DateAdd}. */ + @Contract("_ -> new") public DateDiff withTimezone(Timezone timezone) { return new DateDiff(appendTimezone(argumentMap(), timezone)); } @@ -3073,6 +3132,7 @@ public DateDiff withTimezone(Timezone timezone) { * @param day must not be {@literal null}. * @return new instance of {@link DateDiff}. */ + @Contract("_ -> new") public DateDiff startOfWeek(Object day) { return new DateDiff(append("startOfWeek", day)); } @@ -3132,6 +3192,7 @@ public static DateTrunc truncateValue(Object value) { * @param unit must not be {@literal null}. * @return new instance of {@link DateTrunc}. */ + @Contract("_ -> new") public DateTrunc to(String unit) { return new DateTrunc(append("unit", unit)); } @@ -3142,6 +3203,7 @@ public DateTrunc to(String unit) { * @param unit must not be {@literal null}. * @return new instance of {@link DateTrunc}. */ + @Contract("_ -> new") public DateTrunc to(AggregationExpression unit) { return new DateTrunc(append("unit", unit)); } @@ -3152,6 +3214,7 @@ public DateTrunc to(AggregationExpression unit) { * @param day must not be {@literal null}. * @return new instance of {@link DateTrunc}. */ + @Contract("_ -> new") public DateTrunc startOfWeek(java.time.DayOfWeek day) { return startOfWeek(day.name().toLowerCase(Locale.US)); } @@ -3162,6 +3225,7 @@ public DateTrunc startOfWeek(java.time.DayOfWeek day) { * @param day must not be {@literal null}. * @return new instance of {@link DateTrunc}. */ + @Contract("_ -> new") public DateTrunc startOfWeek(String day) { return new DateTrunc(append("startOfWeek", day)); } @@ -3172,6 +3236,7 @@ public DateTrunc startOfWeek(String day) { * @param binSize must not be {@literal null}. * @return new instance of {@link DateTrunc}. */ + @Contract("_ -> new") public DateTrunc binSize(int binSize) { return binSize((Object) binSize); } @@ -3182,6 +3247,7 @@ public DateTrunc binSize(int binSize) { * @param expression must not be {@literal null}. * @return new instance of {@link DateTrunc}. */ + @Contract("_ -> new") public DateTrunc binSize(AggregationExpression expression) { return binSize((Object) expression); } @@ -3192,6 +3258,7 @@ public DateTrunc binSize(AggregationExpression expression) { * @param binSize must not be {@literal null}. * @return new instance of {@link DateTrunc}. */ + @Contract("_ -> new") public DateTrunc binSize(Object binSize) { return new DateTrunc(append("binSize", binSize)); } @@ -3202,6 +3269,7 @@ public DateTrunc binSize(Object binSize) { * @param timezone must not be {@literal null}. Consider {@link Timezone#none()} instead. * @return new instance of {@link DateTrunc}. */ + @Contract("_ -> new") public DateTrunc withTimezone(Timezone timezone) { return new DateTrunc(appendTimezone(argumentMap(), timezone)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DensifyOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DensifyOperation.java index 0da9343ddf..1a559fd26e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DensifyOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DensifyOperation.java @@ -25,7 +25,8 @@ import java.util.stream.Collectors; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -60,6 +61,9 @@ public static DensifyOperationBuilder builder() { @Override public Document toDocument(AggregationOperationContext context) { + Assert.notNull(field, "Field must be set first"); + Assert.notNull(range, "Range must be set first"); + Document densify = new Document(); densify.put("field", context.getReference(field).getRaw()); if (!ObjectUtils.isEmpty(partitionBy)) { @@ -149,9 +153,9 @@ default Document toDocument() { public static abstract class DensifyRange implements Range { private @Nullable DensifyUnit unit; - private Number step; + private @Nullable Number step; - public DensifyRange(DensifyUnit unit) { + public DensifyRange(@Nullable DensifyUnit unit) { this.unit = unit; } @@ -172,6 +176,7 @@ public Document toDocument(AggregationOperationContext ctx) { * @param step must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public DensifyRange incrementBy(Number step) { this.step = step; return this; @@ -183,6 +188,7 @@ public DensifyRange incrementBy(Number step) { * @param step must not be {@literal null}. * @return this. */ + @Contract("_, _ -> this") public DensifyRange incrementBy(Number step, DensifyUnit unit) { this.step = step; return unit(unit); @@ -194,6 +200,7 @@ public DensifyRange incrementBy(Number step, DensifyUnit unit) { * @param unit * @return this. */ + @Contract("_ -> this") public DensifyRange unit(DensifyUnit unit) { this.unit = unit; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentEnhancingOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentEnhancingOperation.java index 7f260c3785..431215e852 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentEnhancingOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentEnhancingOperation.java @@ -22,6 +22,7 @@ import java.util.stream.Collectors; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation; import org.springframework.util.Assert; @@ -105,7 +106,11 @@ private static Document toSetEntry(Entry entry, AggregationOpera return new Document(field, value); } - private static Object computeValue(Object value, AggregationOperationContext context) { + private static @Nullable Object computeValue(@Nullable Object value, AggregationOperationContext context) { + + if(value == null) { + return value; + } if (value instanceof Field field) { return context.getReference(field).toString(); @@ -154,7 +159,7 @@ static class ExpressionProjection { this.params = parameters.clone(); } - Object toExpression(AggregationOperationContext context) { + @Nullable Object toExpression(AggregationOperationContext context) { return TRANSFORMER.transform(expression, context, params); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentOperators.java index ff63ad834d..a0cd1d056a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentOperators.java @@ -18,6 +18,7 @@ import java.util.Collections; import org.bson.Document; +import org.springframework.lang.Contract; /** * Gateway to {@literal document expressions} such as {@literal $rank, $documentNumber, etc.} @@ -190,6 +191,7 @@ public static Shift shift(AggregationExpression expression) { * @param shiftBy value to add to the current position. * @return new instance of {@link Shift}. */ + @Contract("_ -> new") public Shift by(int shiftBy) { return new Shift(append("by", shiftBy)); } @@ -200,6 +202,7 @@ public Shift by(int shiftBy) { * @param value must not be {@literal null}. * @return new instance of {@link Shift}. */ + @Contract("_ -> new") public Shift defaultTo(Object value) { return new Shift(append("default", value)); } @@ -210,6 +213,7 @@ public Shift defaultTo(Object value) { * @param expression must not be {@literal null}. * @return new instance of {@link Shift}. */ + @Contract("_ -> new") public Shift defaultToValueOf(AggregationExpression expression) { return defaultTo(expression); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperators.java index 56f20dde17..dfdc2d620c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperators.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core.aggregation; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.util.Assert; @@ -50,8 +51,8 @@ public static EvaluationOperatorFactory valueOf(AggregationExpression expression public static class EvaluationOperatorFactory { - private final String fieldReference; - private final AggregationExpression expression; + private final @Nullable String fieldReference; + private final @Nullable AggregationExpression expression; /** * Creates new {@link EvaluationOperatorFactory} for given {@literal fieldReference}. @@ -82,6 +83,7 @@ public EvaluationOperatorFactory(AggregationExpression expression) { * * @return new instance of {@link Expr}. */ + @SuppressWarnings("NullAway") public Expr expr() { return usesFieldRef() ? Expr.valueOf(fieldReference) : Expr.valueOf(expression); } @@ -91,6 +93,7 @@ public Expr expr() { * * @return new instance of {@link Expr}. */ + @SuppressWarnings("NullAway") public LastObservationCarriedForward locf() { return usesFieldRef() ? LastObservationCarriedForward.locfValueOf(fieldReference) : LastObservationCarriedForward.locfValueOf(expression); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java index 458bc43437..703d5d5f06 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java @@ -21,8 +21,8 @@ import java.util.Iterator; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CompositeIterator; import org.springframework.util.ObjectUtils; @@ -154,8 +154,7 @@ public ExposedFields and(ExposedField field) { * @param name must not be {@literal null}. * @return can be {@literal null}. */ - @Nullable - public ExposedField getField(String name) { + public @Nullable ExposedField getField(String name) { for (ExposedField field : this) { if (field.canBeReferredToBy(name)) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java index 131fa8a845..1639a54d48 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java @@ -17,11 +17,10 @@ import org.bson.Document; import org.bson.codecs.configuration.CodecRegistry; - +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -119,8 +118,7 @@ private FieldReference getReference(@Nullable Field field, String name) { * @param name must not be {@literal null}. * @return the resolved reference or {@literal null}. */ - @Nullable - protected FieldReference resolveExposedField(@Nullable Field field, String name) { + protected @Nullable FieldReference resolveExposedField(@Nullable Field field, String name) { ExposedField exposedField = exposedFields.getField(name); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/FacetOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/FacetOperation.java index f5c73dd09c..d1ca95f659 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/FacetOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/FacetOperation.java @@ -23,6 +23,7 @@ import org.bson.Document; import org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.Output; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java index 83fc7c2b87..7f574a850e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java @@ -23,8 +23,9 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.FieldName; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -145,14 +146,17 @@ private Fields(Fields existing, Field tail) { * @param name must not be {@literal null}. * @return */ + @Contract("_ -> new") public Fields and(String name) { return and(new AggregationField(name)); } + @Contract("_ -> new") public Fields and(String name, String target) { return and(new AggregationField(name, target)); } + @Contract("_ -> new") public Fields and(Field field) { return new Fields(this, field); } @@ -172,8 +176,7 @@ public int size() { return fields.size(); } - @Nullable - public Field getField(String name) { + public @Nullable Field getField(String name) { for (Field field : fields) { if (field.getName().equals(name)) { @@ -206,7 +209,7 @@ static class AggregationField implements Field { private final String raw; private final String name; - private final String target; + private final @Nullable String target; /** * Creates an aggregation field with the given {@code name}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java index f4a5fb4498..bcfc64f2b4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java @@ -19,8 +19,9 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.NearQuery; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -80,6 +81,7 @@ private GeoNearOperation(NearQuery nearQuery, String distanceField, @Nullable St * @return new instance of {@link GeoNearOperation}. * @since 2.1 */ + @Contract("_ -> new") public GeoNearOperation useIndex(String key) { return new GeoNearOperation(nearQuery, distanceField, key); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperation.java index 72a917c599..ad1f8ae643 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperation.java @@ -21,10 +21,11 @@ import java.util.Set; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation; import org.springframework.data.mongodb.core.query.CriteriaDefinition; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -35,14 +36,16 @@ * We recommend to use the static factory method {@link Aggregation#graphLookup(String)} instead of creating instances * of this class directly. * - * @see https://docs.mongodb.org/manual/reference/aggregation/graphLookup/ + * @see https://docs.mongodb.org/manual/reference/aggregation/graphLookup/ * @author Mark Paluch * @author Christoph Strobl * @since 1.10 */ public class GraphLookupOperation implements InheritsFieldsAggregationOperation { - private static final Set> ALLOWED_START_TYPES = Set.of(AggregationExpression.class, String.class, Field.class, Document.class); + private static final Set> ALLOWED_START_TYPES = Set.of(AggregationExpression.class, String.class, + Field.class, Document.class); private final String from; private final List startWith; @@ -126,7 +129,7 @@ public ExposedFields getFields() { List fields = new ArrayList<>(2); fields.add(new ExposedField(as, true)); - if(depthField != null) { + if (depthField != null) { fields.add(new ExposedField(depthField, true)); } return ExposedFields.from(fields.toArray(new ExposedField[0])); @@ -217,10 +220,11 @@ static final class GraphLookupOperationFromBuilder implements FromBuilder, StartWithBuilder, ConnectFromBuilder, ConnectToBuilder { private @Nullable String from; - private @Nullable List startWith; + private @Nullable List startWith; private @Nullable String connectFrom; @Override + @Contract("_ -> this") public StartWithBuilder from(String collectionName) { Assert.hasText(collectionName, "CollectionName must not be null or empty"); @@ -230,6 +234,7 @@ public StartWithBuilder from(String collectionName) { } @Override + @Contract("_ -> this") public ConnectFromBuilder startWith(String... fieldReferences) { Assert.notNull(fieldReferences, "FieldReferences must not be null"); @@ -246,6 +251,7 @@ public ConnectFromBuilder startWith(String... fieldReferences) { } @Override + @Contract("_ -> this") public ConnectFromBuilder startWith(AggregationExpression... expressions) { Assert.notNull(expressions, "AggregationExpressions must not be null"); @@ -256,6 +262,7 @@ public ConnectFromBuilder startWith(AggregationExpression... expressions) { } @Override + @Contract("_ -> this") public ConnectFromBuilder startWith(Object... expressions) { Assert.notNull(expressions, "Expressions must not be null"); @@ -297,6 +304,7 @@ private void assertStartWithType(Object expression) { } @Override + @Contract("_ -> this") public ConnectToBuilder connectFrom(String fieldName) { Assert.hasText(fieldName, "ConnectFrom must not be null or empty"); @@ -306,10 +314,14 @@ public ConnectToBuilder connectFrom(String fieldName) { } @Override + @Contract("_ -> new") public GraphLookupOperationBuilder connectTo(String fieldName) { Assert.hasText(fieldName, "ConnectTo must not be null or empty"); + Assert.notNull(from, "From must not be null"); + Assert.notNull(startWith, "startWith must ne set first"); + Assert.notNull(connectFrom, "ConnectFrom must be set first"); return new GraphLookupOperationBuilder(from, startWith, connectFrom, fieldName); } } @@ -327,8 +339,7 @@ public static final class GraphLookupOperationBuilder { private @Nullable Field depthField; private @Nullable CriteriaDefinition restrictSearchWithMatch; - private GraphLookupOperationBuilder(String from, List startWith, String connectFrom, - String connectTo) { + private GraphLookupOperationBuilder(String from, List startWith, String connectFrom, String connectTo) { this.from = from; this.startWith = new ArrayList<>(startWith); @@ -342,6 +353,7 @@ private GraphLookupOperationBuilder(String from, List startWit * @param numberOfRecursions must be greater or equal to zero. * @return this. */ + @Contract("_ -> this") public GraphLookupOperationBuilder maxDepth(long numberOfRecursions) { Assert.isTrue(numberOfRecursions >= 0, "Max depth must be >= 0"); @@ -356,6 +368,7 @@ public GraphLookupOperationBuilder maxDepth(long numberOfRecursions) { * @param fieldName must not be {@literal null} or empty. * @return this. */ + @Contract("_ -> this") public GraphLookupOperationBuilder depthField(String fieldName) { Assert.hasText(fieldName, "Depth field name must not be null or empty"); @@ -370,6 +383,7 @@ public GraphLookupOperationBuilder depthField(String fieldName) { * @param criteriaDefinition must not be {@literal null}. * @return */ + @Contract("_ -> this") public GraphLookupOperationBuilder restrict(CriteriaDefinition criteriaDefinition) { Assert.notNull(criteriaDefinition, "CriteriaDefinition must not be null"); @@ -385,6 +399,7 @@ public GraphLookupOperationBuilder restrict(CriteriaDefinition criteriaDefinitio * @param fieldName must not be {@literal null} or empty. * @return the final {@link GraphLookupOperation}. */ + @Contract("_ -> new") public GraphLookupOperation as(String fieldName) { Assert.hasText(fieldName, "As field name must not be null or empty"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java index 10d58a7682..b6d36f1baf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java @@ -20,10 +20,10 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; import org.springframework.data.mongodb.core.aggregation.ScriptOperators.Accumulator; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -497,6 +497,7 @@ public Operation withAlias(String key) { } public ExposedField asField() { + Assert.notNull(key, "Key must be set first"); return new ExposedField(key, true); } @@ -506,10 +507,12 @@ public Document toDocument(AggregationOperationContext context) { if(op == null && value instanceof Document) { return new Document(key, value); } + + Assert.notNull(op, "Operation keyword must be set"); return new Document(key, new Document(op.toString(), value)); } - public Object getValue(AggregationOperationContext context) { + public @Nullable Object getValue(AggregationOperationContext context) { if (reference == null) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/InheritingExposedFieldsAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/InheritingExposedFieldsAggregationOperationContext.java index ca6a2e2754..739b7c52a9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/InheritingExposedFieldsAggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/InheritingExposedFieldsAggregationOperationContext.java @@ -16,8 +16,8 @@ package org.springframework.data.mongodb.core.aggregation; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; -import org.springframework.lang.Nullable; /** * {@link ExposedFieldsAggregationOperationContext} that inherits fields from its parent diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java index 282ffbd9e0..52cd36b5bb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java @@ -18,11 +18,12 @@ import java.util.function.Supplier; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation; import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let; import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let.ExpressionVariable; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -41,17 +42,13 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe private final String from; - @Nullable // - private final Field localField; + private final @Nullable Field localField; - @Nullable // - private final Field foreignField; + private final @Nullable Field foreignField; - @Nullable // - private final Let let; + private final @Nullable Let let; - @Nullable // - private final AggregationPipeline pipeline; + private final @Nullable AggregationPipeline pipeline; private final ExposedField as; @@ -281,6 +278,7 @@ public static FromBuilder newBuilder() { } @Override + @Contract("_ -> this") public LocalFieldBuilder from(String name) { Assert.hasText(name, "'From' must not be null or empty"); @@ -289,6 +287,7 @@ public LocalFieldBuilder from(String name) { } @Override + @Contract("_ -> this") public AsBuilder foreignField(String name) { Assert.hasText(name, "'ForeignField' must not be null or empty"); @@ -297,6 +296,7 @@ public AsBuilder foreignField(String name) { } @Override + @Contract("_ -> this") public ForeignFieldBuilder localField(String name) { Assert.hasText(name, "'LocalField' must not be null or empty"); @@ -305,6 +305,7 @@ public ForeignFieldBuilder localField(String name) { } @Override + @Contract("_ -> this") public PipelineBuilder let(Let let) { Assert.notNull(let, "Let must not be null"); @@ -313,6 +314,7 @@ public PipelineBuilder let(Let let) { } @Override + @Contract("_ -> this") public AsBuilder pipeline(AggregationPipeline pipeline) { Assert.notNull(pipeline, "Pipeline must not be null"); @@ -321,9 +323,11 @@ public AsBuilder pipeline(AggregationPipeline pipeline) { } @Override + @Contract("_ -> new") public LookupOperation as(String name) { Assert.hasText(name, "'As' must not be null or empty"); + Assert.notNull(from, "From must be set first"); as = new ExposedField(Fields.field(name), true); return new LookupOperation(from, localField, foreignField, let, pipeline, as); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MatchOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MatchOperation.java index da1dbfc027..5f736b55a0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MatchOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MatchOperation.java @@ -17,6 +17,7 @@ import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.util.Assert; @@ -37,8 +38,8 @@ */ public class MatchOperation implements AggregationOperation { - private final CriteriaDefinition criteriaDefinition; - private final AggregationExpression expression; + private final @Nullable CriteriaDefinition criteriaDefinition; + private final @Nullable AggregationExpression expression; /** * Creates a new {@link MatchOperation} for the given {@link CriteriaDefinition}. @@ -68,6 +69,7 @@ public MatchOperation(AggregationExpression expression) { } @Override + @SuppressWarnings("NullAway") public Document toDocument(AggregationOperationContext context) { return new Document(getOperator(), diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MergeOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MergeOperation.java index 314f83fc7c..bda9a3330d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MergeOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MergeOperation.java @@ -22,10 +22,11 @@ import java.util.stream.Collectors; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation; import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -415,7 +416,7 @@ public Document toDocument(AggregationOperationContext context) { */ public static class MergeOperationBuilder { - private String collection; + private @Nullable String collection; private @Nullable String database; private UniqueMergeId id = UniqueMergeId.id(); private @Nullable Let let; @@ -430,6 +431,7 @@ public MergeOperationBuilder() {} * @param collection must not be {@literal null} nor empty. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder intoCollection(String collection) { Assert.hasText(collection, "Collection must not be null nor empty"); @@ -444,6 +446,7 @@ public MergeOperationBuilder intoCollection(String collection) { * @param database must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder inDatabase(String database) { this.database = database; @@ -456,6 +459,7 @@ public MergeOperationBuilder inDatabase(String database) { * @param into must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder into(MergeOperationTarget into) { this.database = into.database; @@ -469,6 +473,7 @@ public MergeOperationBuilder into(MergeOperationTarget into) { * @param target must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder target(MergeOperationTarget target) { return into(target); } @@ -482,6 +487,7 @@ public MergeOperationBuilder target(MergeOperationTarget target) { * @param fields must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder on(String... fields) { return id(UniqueMergeId.ofIdFields(fields)); } @@ -493,6 +499,7 @@ public MergeOperationBuilder on(String... fields) { * @param id must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder id(UniqueMergeId id) { this.id = id; @@ -506,6 +513,7 @@ public MergeOperationBuilder id(UniqueMergeId id) { * @param let the variable expressions * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder let(Let let) { this.let = let; @@ -519,6 +527,7 @@ public MergeOperationBuilder let(Let let) { * @param let the variable expressions * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder exposeVariablesOf(Let let) { return let(let); } @@ -529,6 +538,7 @@ public MergeOperationBuilder exposeVariablesOf(Let let) { * @param whenMatched must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder whenMatched(WhenDocumentsMatch whenMatched) { this.whenMatched = whenMatched; @@ -541,6 +551,7 @@ public MergeOperationBuilder whenMatched(WhenDocumentsMatch whenMatched) { * @param whenMatched must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder whenDocumentsMatch(WhenDocumentsMatch whenMatched) { return whenMatched(whenMatched); } @@ -551,6 +562,7 @@ public MergeOperationBuilder whenDocumentsMatch(WhenDocumentsMatch whenMatched) * @param aggregation must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder whenDocumentsMatchApply(Aggregation aggregation) { return whenMatched(WhenDocumentsMatch.updateWith(aggregation)); } @@ -561,6 +573,7 @@ public MergeOperationBuilder whenDocumentsMatchApply(Aggregation aggregation) { * @param whenNotMatched must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder whenNotMatched(WhenDocumentsDontMatch whenNotMatched) { this.whenNotMatched = whenNotMatched; @@ -573,6 +586,7 @@ public MergeOperationBuilder whenNotMatched(WhenDocumentsDontMatch whenNotMatche * @param whenNotMatched must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder whenDocumentsDontMatch(WhenDocumentsDontMatch whenNotMatched) { return whenNotMatched(whenNotMatched); } @@ -580,7 +594,10 @@ public MergeOperationBuilder whenDocumentsDontMatch(WhenDocumentsDontMatch whenN /** * @return new instance of {@link MergeOperation}. */ + @Contract("-> new") public MergeOperation build() { + + Assert.notNull(collection, "Collection must not be null"); return new MergeOperation(new MergeOperationTarget(database, collection), id, let, whenMatched, whenNotMatched); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NestedDelegatingExpressionAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NestedDelegatingExpressionAggregationOperationContext.java index c553a7be02..a5124320f6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NestedDelegatingExpressionAggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NestedDelegatingExpressionAggregationOperationContext.java @@ -20,6 +20,7 @@ import org.bson.Document; import org.bson.codecs.configuration.CodecRegistry; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExpressionFieldReference; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; import org.springframework.util.Assert; @@ -57,7 +58,7 @@ public Document getMappedObject(Document document) { } @Override - public Document getMappedObject(Document document, Class type) { + public Document getMappedObject(Document document, @Nullable Class type) { return delegate.getMappedObject(document, type); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java index 25189241b7..4e21ab7bde 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java @@ -20,6 +20,7 @@ import java.util.Collections; import org.bson.Document; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -277,7 +278,7 @@ public Document toDocument(Object value, AggregationOperationContext context) { return super.toDocument(potentiallyExtractSingleValue(value), context); } - @SuppressWarnings("unchecked") + @SuppressWarnings("NullAway") private Object potentiallyExtractSingleValue(Object value) { if (value instanceof Collection collection && collection.size() == 1) { @@ -385,6 +386,7 @@ public static GetField getField(Field field) { * @param fieldRef must not be {@literal null}. * @return new instance of {@link GetField}. */ + @Contract("_ -> new") public GetField of(String fieldRef) { return of(Fields.field(fieldRef)); } @@ -396,6 +398,7 @@ public GetField of(String fieldRef) { * @param expression must not be {@literal null}. * @return new instance of {@link GetField}. */ + @Contract("_ -> new") public GetField of(AggregationExpression expression) { return of((Object) expression); } @@ -459,6 +462,7 @@ public static SetField field(Field field) { * @param fieldRef must not be {@literal null}. * @return new instance of {@link GetField}. */ + @Contract("_ -> new") public SetField input(String fieldRef) { return input(Fields.field(fieldRef)); } @@ -470,6 +474,7 @@ public SetField input(String fieldRef) { * @param expression must not be {@literal null}. * @return new instance of {@link SetField}. */ + @Contract("_ -> new") public SetField input(AggregationExpression expression) { return input((Object) expression); } @@ -481,6 +486,7 @@ public SetField input(AggregationExpression expression) { * @param fieldRef must not be {@literal null}. * @return new instance of {@link SetField}. */ + @Contract("_ -> new") private SetField input(Object fieldRef) { return new SetField(append("input", fieldRef)); } @@ -491,6 +497,7 @@ private SetField input(Object fieldRef) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link SetField}. */ + @Contract("_ -> new") public SetField toValueOf(String fieldReference) { return toValue(Fields.field(fieldReference)); } @@ -502,6 +509,7 @@ public SetField toValueOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link SetField}. */ + @Contract("_ -> new") public SetField toValueOf(AggregationExpression expression) { return toValue(expression); } @@ -512,6 +520,7 @@ public SetField toValueOf(AggregationExpression expression) { * @param value * @return new instance of {@link SetField}. */ + @Contract("_ -> new") public SetField toValue(Object value) { return new SetField(append("value", value)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java index 51520f0868..7dbed3a855 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java @@ -16,8 +16,9 @@ package org.springframework.data.mongodb.core.aggregation; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -73,6 +74,7 @@ private OutOperation(@Nullable String databaseName, String collectionName, @Null * @return new instance of {@link OutOperation}. * @since 2.2 */ + @Contract("_ -> new") public OutOperation in(@Nullable String database) { return new OutOperation(database, collectionName, uniqueKey, mode); } @@ -102,6 +104,7 @@ public OutOperation in(@Nullable String database) { * @return new instance of {@link OutOperation}. * @since 2.2 */ + @Contract("_ -> new") public OutOperation uniqueKey(@Nullable String key) { Document uniqueKey = key == null ? null : BsonUtils.toDocumentOrElse(key, it -> new Document(it, 1)); @@ -126,6 +129,7 @@ public OutOperation uniqueKey(@Nullable String key) { * @return new instance of {@link OutOperation}. * @since 2.2 */ + @Contract("_ -> new") public OutOperation uniqueKeyOf(Iterable fields) { Assert.notNull(fields, "Fields must not be null"); @@ -144,6 +148,7 @@ public OutOperation uniqueKeyOf(Iterable fields) { * @return new instance of {@link OutOperation}. * @since 2.2 */ + @Contract("_ -> new") public OutOperation mode(OutMode mode) { Assert.notNull(mode, "Mode must not be null"); @@ -158,6 +163,7 @@ public OutOperation mode(OutMode mode) { * @see OutMode#REPLACE_COLLECTION * @since 2.2 */ + @Contract("-> new") public OutOperation replaceCollection() { return mode(OutMode.REPLACE_COLLECTION); } @@ -170,6 +176,7 @@ public OutOperation replaceCollection() { * @see OutMode#REPLACE * @since 2.2 */ + @Contract("-> new") public OutOperation replaceDocuments() { return mode(OutMode.REPLACE); } @@ -182,6 +189,7 @@ public OutOperation replaceDocuments() { * @see OutMode#INSERT * @since 2.2 */ + @Contract("-> new") public OutOperation insertDocuments() { return mode(OutMode.INSERT); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/PrefixingDelegatingAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/PrefixingDelegatingAggregationOperationContext.java index 9524171fed..54ed40b035 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/PrefixingDelegatingAggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/PrefixingDelegatingAggregationOperationContext.java @@ -25,8 +25,8 @@ import org.bson.Document; import org.bson.codecs.configuration.CodecRegistry; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; -import org.springframework.lang.Nullable; /** * {@link AggregationOperationContext} implementation prefixing non-command keys on root level with the given prefix. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java index 35db2214f5..af7cf5bfb2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java @@ -23,13 +23,14 @@ import java.util.stream.Collectors; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Cond; import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.IfNull; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.Fields.AggregationField; import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder.FieldProjection; import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let.ExpressionVariable; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -149,6 +150,7 @@ public ProjectionOperationBuilder and(AggregationExpression expression) { * @param fieldNames must not be {@literal null}. * @return */ + @Contract("_ -> new") public ProjectionOperation andExclude(String... fieldNames) { List excludeProjections = FieldProjection.from(Fields.fields(fieldNames), false); @@ -161,6 +163,7 @@ public ProjectionOperation andExclude(String... fieldNames) { * @param fieldNames must not be {@literal null}. * @return */ + @Contract("_ -> new") public ProjectionOperation andInclude(String... fieldNames) { List projections = FieldProjection.from(Fields.fields(fieldNames), true); @@ -173,6 +176,7 @@ public ProjectionOperation andInclude(String... fieldNames) { * @param fields must not be {@literal null}. * @return */ + @Contract("_ -> new") public ProjectionOperation andInclude(Fields fields) { return new ProjectionOperation(this.projections, FieldProjection.from(fields, true)); } @@ -185,6 +189,7 @@ public ProjectionOperation andInclude(Fields fields) { * @return new instance of {@link ProjectionOperation}. * @since 2.2 */ + @Contract("_ -> new") public ProjectionOperation asArray(String name) { return new ProjectionOperation(Collections.emptyList(), @@ -402,7 +407,7 @@ public Document toDocument(AggregationOperationContext context) { return new Document(getExposedField().getName(), toMongoExpression(context, expression, params)); } - protected static Object toMongoExpression(AggregationOperationContext context, String expression, + protected static @Nullable Object toMongoExpression(AggregationOperationContext context, String expression, Object[] params) { return TRANSFORMER.transform(expression, context, params); } @@ -1780,6 +1785,7 @@ public ArrayProjectionOperationBuilder(ProjectionOperation target) { * @param expression * @return */ + @Contract("_ -> this") public ArrayProjectionOperationBuilder and(AggregationExpression expression) { Assert.notNull(expression, "AggregationExpression must not be null"); @@ -1794,6 +1800,7 @@ public ArrayProjectionOperationBuilder and(AggregationExpression expression) { * @param field * @return */ + @Contract("_ -> this") public ArrayProjectionOperationBuilder and(Field field) { Assert.notNull(field, "Field must not be null"); @@ -1808,6 +1815,7 @@ public ArrayProjectionOperationBuilder and(Field field) { * @param value * @return */ + @Contract("_ -> this") public ArrayProjectionOperationBuilder and(Object value) { this.projections.add(value); @@ -1820,6 +1828,7 @@ public ArrayProjectionOperationBuilder and(Object value) { * @param name The target property name. Must not be {@literal null}. * @return new instance of {@link ArrayProjectionOperationBuilder}. */ + @Contract("_ -> new") public ProjectionOperation as(String name) { return new ProjectionOperation(target.projections, diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RedactOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RedactOperation.java index a370016356..5f16fcfc16 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RedactOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RedactOperation.java @@ -16,8 +16,10 @@ package org.springframework.data.mongodb.core.aggregation; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Cond.ThenBuilder; import org.springframework.data.mongodb.core.query.CriteriaDefinition; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -33,7 +35,8 @@ * * * @author Christoph Strobl - * @see https://docs.mongodb.com/manual/reference/operator/aggregation/redact/ + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/redact/ * @since 3.0 */ public class RedactOperation implements AggregationOperation { @@ -94,9 +97,9 @@ public static RedactOperationBuilder builder() { */ public static class RedactOperationBuilder { - private Object when; - private Object then; - private Object otherwise; + private @Nullable Object when; + private @Nullable Object then; + private @Nullable Object otherwise; private RedactOperationBuilder() { @@ -108,6 +111,7 @@ private RedactOperationBuilder() { * @param criteria must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public RedactOperationBuilder when(CriteriaDefinition criteria) { this.when = criteria; @@ -120,6 +124,7 @@ public RedactOperationBuilder when(CriteriaDefinition criteria) { * @param condition must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public RedactOperationBuilder when(AggregationExpression condition) { this.when = condition; @@ -132,6 +137,7 @@ public RedactOperationBuilder when(AggregationExpression condition) { * @param condition must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public RedactOperationBuilder when(Document condition) { this.when = condition; @@ -143,6 +149,7 @@ public RedactOperationBuilder when(Document condition) { * * @return this. */ + @Contract("-> this") public RedactOperationBuilder thenDescend() { return then(DESCEND); } @@ -152,6 +159,7 @@ public RedactOperationBuilder thenDescend() { * * @return this. */ + @Contract("-> this") public RedactOperationBuilder thenKeep() { return then(KEEP); } @@ -161,6 +169,7 @@ public RedactOperationBuilder thenKeep() { * * @return this. */ + @Contract("-> this") public RedactOperationBuilder thenPrune() { return then(PRUNE); } @@ -172,6 +181,7 @@ public RedactOperationBuilder thenPrune() { * @param then must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public RedactOperationBuilder then(Object then) { this.then = then; @@ -183,6 +193,7 @@ public RedactOperationBuilder then(Object then) { * * @return this. */ + @Contract("-> this") public RedactOperationBuilder otherwiseDescend() { return otherwise(DESCEND); } @@ -192,6 +203,7 @@ public RedactOperationBuilder otherwiseDescend() { * * @return this. */ + @Contract("-> this") public RedactOperationBuilder otherwiseKeep() { return otherwise(KEEP); } @@ -201,6 +213,7 @@ public RedactOperationBuilder otherwiseKeep() { * * @return this. */ + @Contract("-> this") public RedactOperationBuilder otherwisePrune() { return otherwise(PRUNE); } @@ -212,6 +225,7 @@ public RedactOperationBuilder otherwisePrune() { * @param otherwise must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public RedactOperationBuilder otherwise(Object otherwise) { this.otherwise = otherwise; return this; @@ -220,7 +234,12 @@ public RedactOperationBuilder otherwise(Object otherwise) { /** * @return new instance of {@link RedactOperation}. */ + @Contract("-> new") public RedactOperation build() { + + Assert.notNull(then, "Then must be set first"); + Assert.notNull(otherwise, "Otherwise must be set first"); + return new RedactOperation(when().then(then).otherwise(otherwise)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java index 130182a001..ec306eb6c5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java @@ -23,6 +23,7 @@ import org.bson.Document; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.expression.spel.ast.Projection; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -457,6 +458,7 @@ public DocumentContributor(Object value) { } @Override + @SuppressWarnings("NullAway") public Document toDocument(AggregationOperationContext context) { Document document = new Document("$set", value); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ScriptOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ScriptOperators.java index 9eab041e88..ed615d9863 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ScriptOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ScriptOperators.java @@ -22,15 +22,15 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ScriptOperators.Accumulator.AccumulatorBuilder; import org.springframework.data.mongodb.core.aggregation.ScriptOperators.Accumulator.AccumulatorInitBuilder; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; /** - * Gateway to {@literal $function} and {@literal $accumulator} aggregation operations. - *
+ * Gateway to {@literal $function} and {@literal $accumulator} aggregation operations.
* Using {@link ScriptOperators} as part of the {@link Aggregation} requires MongoDB server to have * server-side JavaScript execution * enabled. @@ -53,8 +53,8 @@ public static Function function(String body) { } /** - * Create a custom $accumulator operator - * in Javascript. + * Create a custom $accumulator + * operator in Javascript. * * @return new instance of {@link AccumulatorInitBuilder}. */ @@ -74,8 +74,7 @@ public static AccumulatorInitBuilder accumulatorBuilder() { * lang: "js" * } * } - * - *
+ *
* {@link Function} cannot be used as part of {@link org.springframework.data.mongodb.core.schema.MongoJsonSchema * schema} validation query expression.
* NOTE: Server-Side JavaScript @@ -150,10 +149,12 @@ List getArgs() { return get(Fields.ARGS.toString()); } + @Nullable String getBody() { return get(Fields.BODY.toString()); } + @Nullable String getLang() { return get(Fields.LANG.toString()); } @@ -178,8 +179,7 @@ public String toString() { * {@link Accumulator} defines a custom aggregation * $accumulator operator, * one that maintains its state (e.g. totals, maximums, minimums, and related data) as documents progress through the - * pipeline, in JavaScript. - *
+ * pipeline, in JavaScript.
* * { * $accumulator: { @@ -192,8 +192,7 @@ public String toString() { * lang: "js" * } * } - * - *
+ *
* {@link Accumulator} can be used as part of {@link GroupOperation $group}, {@link BucketOperation $bucket} and * {@link BucketAutoOperation $bucketAuto} pipeline stages.
* NOTE: Server-Side JavaScript @@ -240,8 +239,7 @@ public interface AccumulatorInitBuilder { /** * Define the {@code init} {@link Function} for the {@link Accumulator accumulators} initial state. The function - * receives its arguments from the {@link Function#args(Object...) initArgs} array expression. - *
+ * receives its arguments from the {@link Function#args(Object...) initArgs} array expression.
* * function(initArg1, initArg2, ...) { * ... @@ -253,13 +251,16 @@ public interface AccumulatorInitBuilder { * @return this. */ default AccumulatorAccumulateBuilder init(Function function) { - return init(function.getBody()).initArgs(function.getArgs()); + + Assert.notNull(function.getBody(), "Function body must not be null"); + + List args = function.getArgs(); + return init(function.getBody()).initArgs(args != null ? args : List.of()); } /** * Define the {@code init} function for the {@link Accumulator accumulators} initial state. The function receives - * its arguments from the {@link AccumulatorInitArgsBuilder#initArgs(Object...)} array expression. - *
+ * its arguments from the {@link AccumulatorInitArgsBuilder#initArgs(Object...)} array expression.
* * function(initArg1, initArg2, ...) { * ... @@ -307,8 +308,7 @@ public interface AccumulatorAccumulateBuilder { /** * Set the {@code accumulate} {@link Function} that updates the state for each document. The functions first * argument is the current {@code state}, additional arguments can be defined via {@link Function#args(Object...) - * accumulateArgs}. - *
+ * accumulateArgs}.
* * function(state, accumArg1, accumArg2, ...) { * ... @@ -320,14 +320,17 @@ public interface AccumulatorAccumulateBuilder { * @return this. */ default AccumulatorMergeBuilder accumulate(Function function) { - return accumulate(function.getBody()).accumulateArgs(function.getArgs()); + + Assert.notNull(function.getBody(), "Function body must not be null"); + + List args = function.getArgs(); + return accumulate(function.getBody()).accumulateArgs(args != null ? args : List.of()); } /** * Set the {@code accumulate} function that updates the state for each document. The functions first argument is * the current {@code state}, additional arguments can be defined via - * {@link AccumulatorAccumulateArgsBuilder#accumulateArgs(Object...)}. - *
+ * {@link AccumulatorAccumulateArgsBuilder#accumulateArgs(Object...)}.
* * function(state, accumArg1, accumArg2, ...) { * ... @@ -369,8 +372,7 @@ public interface AccumulatorMergeBuilder { /** * Set the {@code merge} function used to merge two internal states.
* This might be required because the operation is run on a sharded cluster or when the operator exceeds its - * memory limit. - *
+ * memory limit.
* * function(state1, state2) { * ... @@ -388,8 +390,7 @@ public interface AccumulatorFinalizeBuilder { /** * Set the {@code finalize} function used to update the result of the accumulation when all documents have been - * processed. - *
+ * processed.
* * function(state) { * ... @@ -414,18 +415,17 @@ static class AccumulatorBuilder implements AccumulatorInitBuilder, AccumulatorInitArgsBuilder, AccumulatorAccumulateBuilder, AccumulatorAccumulateArgsBuilder, AccumulatorMergeBuilder, AccumulatorFinalizeBuilder { - private List initArgs; - private String initFunction; - private List accumulateArgs; - private String accumulateFunction; - private String mergeFunction; - private String finalizeFunction; + private @Nullable List initArgs; + private @Nullable String initFunction; + private @Nullable List accumulateArgs; + private @Nullable String accumulateFunction; + private @Nullable String mergeFunction; + private @Nullable String finalizeFunction; private String lang = "js"; /** * Define the {@code init} function for the {@link Accumulator accumulators} initial state. The function receives - * its arguments from the {@link #initArgs(Object...)} array expression. - *
+ * its arguments from the {@link #initArgs(Object...)} array expression.
* * function(initArg1, initArg2, ...) { * ... @@ -437,6 +437,7 @@ static class AccumulatorBuilder * @return this. */ @Override + @Contract("_ -> this") public AccumulatorBuilder init(String function) { this.initFunction = function; @@ -450,6 +451,7 @@ public AccumulatorBuilder init(String function) { * @return this. */ @Override + @Contract("_ -> this") public AccumulatorBuilder initArgs(List args) { Assert.notNull(args, "Args must not be null"); @@ -460,8 +462,7 @@ public AccumulatorBuilder initArgs(List args) { /** * Set the {@code accumulate} function that updates the state for each document. The functions first argument is - * the current {@code state}, additional arguments can be defined via {@link #accumulateArgs(Object...)}. - *
+ * the current {@code state}, additional arguments can be defined via {@link #accumulateArgs(Object...)}.
* * function(state, accumArg1, accumArg2, ...) { * ... @@ -473,6 +474,7 @@ public AccumulatorBuilder initArgs(List args) { * @return this. */ @Override + @Contract("_ -> this") public AccumulatorBuilder accumulate(String function) { Assert.notNull(function, "Accumulate function must not be null"); @@ -488,6 +490,7 @@ public AccumulatorBuilder accumulate(String function) { * @return this. */ @Override + @Contract("_ -> this") public AccumulatorBuilder accumulateArgs(List args) { Assert.notNull(args, "Args must not be null"); @@ -499,8 +502,7 @@ public AccumulatorBuilder accumulateArgs(List args) { /** * Set the {@code merge} function used to merge two internal states.
* This might be required because the operation is run on a sharded cluster or when the operator exceeds its - * memory limit. - *
+ * memory limit.
* * function(state1, state2) { * ... @@ -512,6 +514,7 @@ public AccumulatorBuilder accumulateArgs(List args) { * @return this. */ @Override + @Contract("_ -> this") public AccumulatorBuilder merge(String function) { Assert.notNull(function, "Merge function must not be null"); @@ -526,6 +529,7 @@ public AccumulatorBuilder merge(String function) { * @param lang must not be {@literal null}. Default is {@literal js}. * @return this. */ + @Contract("_ -> this") public AccumulatorBuilder lang(String lang) { Assert.hasText(lang, "Lang must not be null nor empty; The default would be 'js'"); @@ -536,8 +540,7 @@ public AccumulatorBuilder lang(String lang) { /** * Set the {@code finalize} function used to update the result of the accumulation when all documents have been - * processed. - *
+ * processed.
* * function(state) { * ... @@ -549,6 +552,7 @@ public AccumulatorBuilder lang(String lang) { * @return new instance of {@link Accumulator}. */ @Override + @Contract("_ -> new") public Accumulator finalize(String function) { Assert.notNull(function, "Finalize function must not be null"); @@ -562,6 +566,7 @@ public Accumulator finalize(String function) { } @Override + @Contract("-> new") public Accumulator build() { return new Accumulator(createArgumentMap()); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java index 9da80c4668..4db0181d39 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java @@ -19,6 +19,7 @@ import java.util.Collections; import org.springframework.data.domain.Sort; +import org.springframework.lang.Contract; /** * Gateway to {@literal selection operators} such as {@literal $bottom}. @@ -69,6 +70,7 @@ public static Bottom bottom(int numberOfResults) { * @param numberOfResults * @return new instance of {@link Bottom}. */ + @Contract("_ -> new") public Bottom limit(int numberOfResults) { return limit((Object) numberOfResults); } @@ -80,10 +82,12 @@ public Bottom limit(int numberOfResults) { * @param expression must not be {@literal null}. * @return new instance of {@link Bottom}. */ + @Contract("_ -> new") public Bottom limit(AggregationExpression expression) { return limit((Object) expression); } + @Contract("_ -> new") private Bottom limit(Object value) { return new Bottom(append("n", value)); } @@ -94,6 +98,7 @@ private Bottom limit(Object value) { * @param sort must not be {@literal null}. * @return new instance of {@link Bottom}. */ + @Contract("_ -> new") public Bottom sortBy(Sort sort) { return new Bottom(append("sortBy", sort)); } @@ -104,6 +109,7 @@ public Bottom sortBy(Sort sort) { * @param out must not be {@literal null}. * @return new instance of {@link Bottom}. */ + @Contract("_ -> new") public Bottom output(Fields out) { return new Bottom(append("output", out)); } @@ -115,6 +121,7 @@ public Bottom output(Fields out) { * @return new instance of {@link Bottom}. * @see #output(Fields) */ + @Contract("_ -> new") public Bottom output(String... fieldNames) { return output(Fields.fields(fieldNames)); } @@ -126,6 +133,7 @@ public Bottom output(String... fieldNames) { * @return new instance of {@link Bottom}. * @see #output(Fields) */ + @Contract("_ -> new") public Bottom output(AggregationExpression... out) { return new Bottom(append("output", Arrays.asList(out))); } @@ -172,6 +180,7 @@ public static Top top(int numberOfResults) { * @param numberOfResults * @return new instance of {@link Top}. */ + @Contract("_ -> new") public Top limit(int numberOfResults) { return limit((Object) numberOfResults); } @@ -183,6 +192,7 @@ public Top limit(int numberOfResults) { * @param expression must not be {@literal null}. * @return new instance of {@link Top}. */ + @Contract("_ -> new") public Top limit(AggregationExpression expression) { return limit((Object) expression); } @@ -197,6 +207,7 @@ private Top limit(Object value) { * @param sort must not be {@literal null}. * @return new instance of {@link Top}. */ + @Contract("_ -> new") public Top sortBy(Sort sort) { return new Top(append("sortBy", sort)); } @@ -207,6 +218,7 @@ public Top sortBy(Sort sort) { * @param out must not be {@literal null}. * @return new instance of {@link Top}. */ + @Contract("_ -> new") public Top output(Fields out) { return new Top(append("output", out)); } @@ -218,6 +230,7 @@ public Top output(Fields out) { * @return new instance of {@link Top}. * @see #output(Fields) */ + @Contract("_ -> new") public Top output(String... fieldNames) { return output(Fields.fields(fieldNames)); } @@ -229,6 +242,7 @@ public Top output(String... fieldNames) { * @return new instance of {@link Top}. * @see #output(Fields) */ + @Contract("_ -> new") public Top output(AggregationExpression... out) { return new Top(append("output", Arrays.asList(out))); } @@ -263,6 +277,7 @@ public static First first(int numberOfResults) { * @param numberOfResults * @return new instance of {@link First}. */ + @Contract("_ -> new") public First limit(int numberOfResults) { return limit((Object) numberOfResults); } @@ -274,6 +289,7 @@ public First limit(int numberOfResults) { * @param expression must not be {@literal null}. * @return new instance of {@link First}. */ + @Contract("_ -> new") public First limit(AggregationExpression expression) { return limit((Object) expression); } @@ -288,6 +304,7 @@ private First limit(Object value) { * @param fieldName must not be {@literal null}. * @return new instance of {@link First}. */ + @Contract("_ -> new") public First of(String fieldName) { return input(fieldName); } @@ -298,6 +315,7 @@ public First of(String fieldName) { * @param expression must not be {@literal null}. * @return new instance of {@link First}. */ + @Contract("_ -> new") public First of(AggregationExpression expression) { return input(expression); } @@ -308,6 +326,7 @@ public First of(AggregationExpression expression) { * @param fieldName must not be {@literal null}. * @return new instance of {@link First}. */ + @Contract("_ -> new") public First input(String fieldName) { return new First(append("input", Fields.field(fieldName))); } @@ -318,6 +337,7 @@ public First input(String fieldName) { * @param expression must not be {@literal null}. * @return new instance of {@link First}. */ + @Contract("_ -> new") public First input(AggregationExpression expression) { return new First(append("input", expression)); } @@ -357,6 +377,7 @@ public static Last last(int numberOfResults) { * @param numberOfResults * @return new instance of {@link Last}. */ + @Contract("_ -> new") public Last limit(int numberOfResults) { return limit((Object) numberOfResults); } @@ -368,6 +389,7 @@ public Last limit(int numberOfResults) { * @param expression must not be {@literal null}. * @return new instance of {@link Last}. */ + @Contract("_ -> new") public Last limit(AggregationExpression expression) { return limit((Object) expression); } @@ -382,6 +404,7 @@ private Last limit(Object value) { * @param fieldName must not be {@literal null}. * @return new instance of {@link Last}. */ + @Contract("_ -> new") public Last of(String fieldName) { return input(fieldName); } @@ -392,6 +415,7 @@ public Last of(String fieldName) { * @param expression must not be {@literal null}. * @return new instance of {@link Last}. */ + @Contract("_ -> new") public Last of(AggregationExpression expression) { return input(expression); } @@ -402,6 +426,7 @@ public Last of(AggregationExpression expression) { * @param fieldName must not be {@literal null}. * @return new instance of {@link Last}. */ + @Contract("_ -> new") public Last input(String fieldName) { return new Last(append("input", Fields.field(fieldName))); } @@ -412,6 +437,7 @@ public Last input(String fieldName) { * @param expression must not be {@literal null}. * @return new instance of {@link Last}. */ + @Contract("_ -> new") public Last input(AggregationExpression expression) { return new Last(append("input", expression)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java index 7f5c1c7722..6ef4f1323f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java @@ -19,8 +19,9 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.SetOperation.FieldAppender.ValueAppender; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; /** * Adds new fields to documents. {@code $set} outputs documents that contain all existing fields from the input @@ -82,6 +83,7 @@ public static ValueAppender set(String field) { * @param value the value to assign. * @return new instance of {@link SetOperation}. */ + @Contract("_, _ -> new") public SetOperation set(Object field, Object value) { LinkedHashMap target = new LinkedHashMap<>(getValueMap()); @@ -131,7 +133,7 @@ public ValueAppender set(String field) { return new ValueAppender() { @Override - public SetOperation toValue(Object value) { + public SetOperation toValue(@Nullable Object value) { valueMap.put(field, value); return FieldAppender.this.build(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperators.java index 094ef7365b..a99c0926f4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperators.java @@ -19,7 +19,9 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Sum; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -55,8 +57,8 @@ public static SetOperatorFactory arrayAsSet(AggregationExpression expression) { */ public static class SetOperatorFactory { - private final String fieldReference; - private final AggregationExpression expression; + private final @Nullable String fieldReference; + private final @Nullable AggregationExpression expression; /** * Creates new {@link SetOperatorFactory} for given {@literal fieldReference}. @@ -104,6 +106,7 @@ public SetEquals isEqualTo(AggregationExpression... expressions) { return createSetEquals().isEqualTo(expressions); } + @SuppressWarnings("NullAway") private SetEquals createSetEquals() { return usesFieldRef() ? SetEquals.arrayAsSet(fieldReference) : SetEquals.arrayAsSet(expression); } @@ -130,6 +133,7 @@ public SetIntersection intersects(AggregationExpression... expressions) { return createSetIntersection().intersects(expressions); } + @SuppressWarnings("NullAway") private SetIntersection createSetIntersection() { return usesFieldRef() ? SetIntersection.arrayAsSet(fieldReference) : SetIntersection.arrayAsSet(expression); } @@ -156,6 +160,7 @@ public SetUnion union(AggregationExpression... expressions) { return createSetUnion().union(expressions); } + @SuppressWarnings("NullAway") private SetUnion createSetUnion() { return usesFieldRef() ? SetUnion.arrayAsSet(fieldReference) : SetUnion.arrayAsSet(expression); } @@ -182,6 +187,7 @@ public SetDifference differenceTo(AggregationExpression expression) { return createSetDifference().differenceTo(expression); } + @SuppressWarnings("NullAway") private SetDifference createSetDifference() { return usesFieldRef() ? SetDifference.arrayAsSet(fieldReference) : SetDifference.arrayAsSet(expression); } @@ -208,6 +214,7 @@ public SetIsSubset isSubsetOf(AggregationExpression expression) { return createSetIsSubset().isSubsetOf(expression); } + @SuppressWarnings("NullAway") private SetIsSubset createSetIsSubset() { return usesFieldRef() ? SetIsSubset.arrayAsSet(fieldReference) : SetIsSubset.arrayAsSet(expression); } @@ -218,6 +225,7 @@ private SetIsSubset createSetIsSubset() { * * @return new instance of {@link AnyElementTrue}. */ + @SuppressWarnings("NullAway") public AnyElementTrue anyElementTrue() { return usesFieldRef() ? AnyElementTrue.arrayAsSet(fieldReference) : AnyElementTrue.arrayAsSet(expression); } @@ -228,6 +236,7 @@ public AnyElementTrue anyElementTrue() { * * @return new instance of {@link AllElementsTrue}. */ + @SuppressWarnings("NullAway") public AllElementsTrue allElementsTrue() { return usesFieldRef() ? AllElementsTrue.arrayAsSet(fieldReference) : AllElementsTrue.arrayAsSet(expression); } @@ -283,6 +292,7 @@ public static SetEquals arrayAsSet(AggregationExpression expression) { * @param arrayReferences must not be {@literal null}. * @return new instance of {@link SetEquals}. */ + @Contract("_ -> new") public SetEquals isEqualTo(String... arrayReferences) { Assert.notNull(arrayReferences, "ArrayReferences must not be null"); @@ -295,6 +305,7 @@ public SetEquals isEqualTo(String... arrayReferences) { * @param expressions must not be {@literal null}. * @return new instance of {@link SetEquals}. */ + @Contract("_ -> new") public SetEquals isEqualTo(AggregationExpression... expressions) { Assert.notNull(expressions, "Expressions must not be null"); @@ -307,6 +318,7 @@ public SetEquals isEqualTo(AggregationExpression... expressions) { * @param array must not be {@literal null}. * @return new instance of {@link SetEquals}. */ + @Contract("_ -> new") public SetEquals isEqualTo(Object[] array) { Assert.notNull(array, "Array must not be null"); @@ -360,6 +372,7 @@ public static SetIntersection arrayAsSet(AggregationExpression expression) { * @param arrayReferences must not be {@literal null}. * @return new instance of {@link SetIntersection}. */ + @Contract("_ -> new") public SetIntersection intersects(String... arrayReferences) { Assert.notNull(arrayReferences, "ArrayReferences must not be null"); @@ -372,6 +385,7 @@ public SetIntersection intersects(String... arrayReferences) { * @param expressions must not be {@literal null}. * @return new instance of {@link SetIntersection}. */ + @Contract("_ -> new") public SetIntersection intersects(AggregationExpression... expressions) { Assert.notNull(expressions, "Expressions must not be null"); @@ -425,6 +439,7 @@ public static SetUnion arrayAsSet(AggregationExpression expression) { * @param arrayReferences must not be {@literal null}. * @return new instance of {@link SetUnion}. */ + @Contract("_ -> new") public SetUnion union(String... arrayReferences) { Assert.notNull(arrayReferences, "ArrayReferences must not be null"); @@ -437,6 +452,7 @@ public SetUnion union(String... arrayReferences) { * @param expressions must not be {@literal null}. * @return new instance of {@link SetUnion}. */ + @Contract("_ -> new") public SetUnion union(AggregationExpression... expressions) { Assert.notNull(expressions, "Expressions must not be null"); @@ -490,6 +506,7 @@ public static SetDifference arrayAsSet(AggregationExpression expression) { * @param arrayReference must not be {@literal null}. * @return new instance of {@link SetDifference}. */ + @Contract("_ -> new") public SetDifference differenceTo(String arrayReference) { Assert.notNull(arrayReference, "ArrayReference must not be null"); @@ -502,6 +519,7 @@ public SetDifference differenceTo(String arrayReference) { * @param expression must not be {@literal null}. * @return new instance of {@link SetDifference}. */ + @Contract("_ -> new") public SetDifference differenceTo(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -555,6 +573,7 @@ public static SetIsSubset arrayAsSet(AggregationExpression expression) { * @param arrayReference must not be {@literal null}. * @return new instance of {@link SetIsSubset}. */ + @Contract("_ -> new") public SetIsSubset isSubsetOf(String arrayReference) { Assert.notNull(arrayReference, "ArrayReference must not be null"); @@ -567,6 +586,7 @@ public SetIsSubset isSubsetOf(String arrayReference) { * @param expression must not be {@literal null}. * @return new instance of {@link SetIsSubset}. */ + @Contract("_ -> new") public SetIsSubset isSubsetOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -614,6 +634,7 @@ public static AnyElementTrue arrayAsSet(AggregationExpression expression) { return new AnyElementTrue(Collections.singletonList(expression)); } + @Contract("-> this") public AnyElementTrue anyElementTrue() { return this; } @@ -659,6 +680,7 @@ public static AllElementsTrue arrayAsSet(AggregationExpression expression) { return new AllElementsTrue(Collections.singletonList(expression)); } + @Contract("-> this") public AllElementsTrue allElementsTrue() { return this; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperation.java index 2b8df539e1..e1fec17811 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperation.java @@ -22,8 +22,9 @@ import java.util.concurrent.TimeUnit; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -31,7 +32,8 @@ * * @author Christoph Strobl * @since 3.3 - * @see https://docs.mongodb.com/manual/reference/operator/aggregation/setWindowFields/ + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/setWindowFields/ */ public class SetWindowFieldsOperation implements AggregationOperation, FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation { @@ -137,6 +139,7 @@ public WindowOutput(ComputedField outputField) { * @param field must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public WindowOutput append(ComputedField field) { Assert.notNull(field, "Field must not be null"); @@ -152,6 +155,7 @@ public WindowOutput append(ComputedField field) { * @return new instance of {@link ComputedFieldAppender}. * @see #append(ComputedField) */ + @Contract("_ -> new") public ComputedFieldAppender append(AggregationExpression expression) { return new ComputedFieldAppender() { @@ -249,8 +253,7 @@ public AggregationExpression getWindowOperator() { return windowOperator; } - @Nullable - public Window getWindow() { + public @Nullable Window getWindow() { return window; } } @@ -360,6 +363,7 @@ public static class RangeWindowBuilder { * @param lower eg. {@literal current} or {@literal unbounded}. * @return this. */ + @Contract("_ -> this") public RangeWindowBuilder from(String lower) { this.lower = lower; @@ -372,6 +376,7 @@ public RangeWindowBuilder from(String lower) { * @param upper eg. {@literal current} or {@literal unbounded}. * @return this. */ + @Contract("_ -> this") public RangeWindowBuilder to(String upper) { this.upper = upper; @@ -386,6 +391,7 @@ public RangeWindowBuilder to(String upper) { * @param lower * @return this. */ + @Contract("_ -> this") public RangeWindowBuilder from(Number lower) { this.lower = lower; @@ -400,6 +406,7 @@ public RangeWindowBuilder from(Number lower) { * @param upper * @return this. */ + @Contract("_ -> this") public RangeWindowBuilder to(Number upper) { this.upper = upper; @@ -411,6 +418,7 @@ public RangeWindowBuilder to(Number upper) { * * @return this. */ + @Contract("-> this") public RangeWindowBuilder fromCurrent() { return from(CURRENT); } @@ -420,6 +428,7 @@ public RangeWindowBuilder fromCurrent() { * * @return this. */ + @Contract("-> this") public RangeWindowBuilder fromUnbounded() { return from(UNBOUNDED); } @@ -429,6 +438,7 @@ public RangeWindowBuilder fromUnbounded() { * * @return this. */ + @Contract("-> this") public RangeWindowBuilder toCurrent() { return to(CURRENT); } @@ -438,6 +448,7 @@ public RangeWindowBuilder toCurrent() { * * @return this. */ + @Contract("-> this") public RangeWindowBuilder toUnbounded() { return to(UNBOUNDED); } @@ -448,6 +459,7 @@ public RangeWindowBuilder toUnbounded() { * @param windowUnit must not be {@literal null}. Can be on of {@link Windows}. * @return this. */ + @Contract("_ -> this") public RangeWindowBuilder unit(WindowUnit windowUnit) { Assert.notNull(windowUnit, "WindowUnit must not be null"); @@ -460,6 +472,7 @@ public RangeWindowBuilder unit(WindowUnit windowUnit) { * * @return new instance of {@link RangeWindow}. */ + @Contract("-> new") public RangeWindow build() { Assert.notNull(lower, "Lower bound must not be null"); @@ -488,20 +501,24 @@ public static class DocumentWindowBuilder { * @param lower * @return this. */ + @Contract("_ -> this") public DocumentWindowBuilder from(Number lower) { this.lower = lower; return this; } + @Contract("-> this") public DocumentWindowBuilder fromCurrent() { return from(CURRENT); } + @Contract("-> this") public DocumentWindowBuilder fromUnbounded() { return from(UNBOUNDED); } + @Contract("-> this") public DocumentWindowBuilder to(String upper) { this.upper = upper; @@ -514,6 +531,7 @@ public DocumentWindowBuilder to(String upper) { * @param lower eg. {@literal current} or {@literal unbounded}. * @return this. */ + @Contract("_ -> this") public DocumentWindowBuilder from(String lower) { this.lower = lower; @@ -528,20 +546,24 @@ public DocumentWindowBuilder from(String lower) { * @param upper * @return this. */ + @Contract("_ -> this") public DocumentWindowBuilder to(Number upper) { this.upper = upper; return this; } + @Contract("-> this") public DocumentWindowBuilder toCurrent() { return to(CURRENT); } + @Contract("-> this") public DocumentWindowBuilder toUnbounded() { return to(UNBOUNDED); } + @Contract("-> new") public DocumentWindow build() { Assert.notNull(lower, "Lower bound must not be null"); @@ -689,9 +711,9 @@ public enum WindowUnits implements WindowUnit { */ public static class SetWindowFieldsOperationBuilder { - private Object partitionBy; - private SortOperation sortOperation; - private WindowOutput output; + private @Nullable Object partitionBy; + private @Nullable SortOperation sortOperation; + private @Nullable WindowOutput output; /** * Specify the field to group by. @@ -699,6 +721,7 @@ public static class SetWindowFieldsOperationBuilder { * @param fieldName must not be {@literal null} or null. * @return this. */ + @Contract("_ -> this") public SetWindowFieldsOperationBuilder partitionByField(String fieldName) { Assert.hasText(fieldName, "Field name must not be empty or null"); @@ -711,6 +734,7 @@ public SetWindowFieldsOperationBuilder partitionByField(String fieldName) { * @param expression must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public SetWindowFieldsOperationBuilder partitionByExpression(AggregationExpression expression) { return partitionBy(expression); } @@ -721,6 +745,7 @@ public SetWindowFieldsOperationBuilder partitionByExpression(AggregationExpressi * @param fields must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public SetWindowFieldsOperationBuilder sortBy(String... fields) { return sortBy(Sort.by(fields)); } @@ -731,6 +756,7 @@ public SetWindowFieldsOperationBuilder sortBy(String... fields) { * @param sort must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public SetWindowFieldsOperationBuilder sortBy(Sort sort) { return sortBy(new SortOperation(sort)); } @@ -741,6 +767,7 @@ public SetWindowFieldsOperationBuilder sortBy(Sort sort) { * @param sort must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public SetWindowFieldsOperationBuilder sortBy(SortOperation sort) { Assert.notNull(sort, "SortOperation must not be null"); @@ -755,6 +782,7 @@ public SetWindowFieldsOperationBuilder sortBy(SortOperation sort) { * @param output must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public SetWindowFieldsOperationBuilder output(WindowOutput output) { Assert.notNull(output, "WindowOutput must not be null"); @@ -769,6 +797,7 @@ public SetWindowFieldsOperationBuilder output(WindowOutput output) { * @param expression must not be {@literal null}. * @return new instance of {@link WindowChoice}. */ + @Contract("_ -> new") public WindowChoice output(AggregationExpression expression) { return new WindowChoice() { @@ -837,6 +866,7 @@ public interface WindowChoice extends As { * @param value must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public SetWindowFieldsOperationBuilder partitionBy(Object value) { Assert.notNull(value, "Partition By must not be null"); @@ -850,7 +880,10 @@ public SetWindowFieldsOperationBuilder partitionBy(Object value) { * * @return new instance of {@link SetWindowFieldsOperation}. */ + @Contract("-> new") public SetWindowFieldsOperation build() { + + Assert.notNull(output, "Output must be set first"); return new SetWindowFieldsOperation(partitionBy, sortOperation, output); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortByCountOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortByCountOperation.java index ffc0aa0654..e6a9a23d31 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortByCountOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortByCountOperation.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core.aggregation; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** @@ -67,6 +67,7 @@ public SortByCountOperation(AggregationExpression groupByExpression) { } @Override + @SuppressWarnings("NullAway") public Document toDocument(AggregationOperationContext context) { return new Document(getOperator(), groupByExpression == null ? context.getReference(groupByField).toString() diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java index 3119e2729c..ade4f5328e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java @@ -20,6 +20,7 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.core.GenericTypeResolver; import org.springframework.data.mongodb.core.spel.ExpressionNode; import org.springframework.data.mongodb.core.spel.ExpressionTransformationContextSupport; @@ -42,7 +43,6 @@ import org.springframework.expression.spel.standard.SpelExpression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.NumberUtils; import org.springframework.util.ObjectUtils; @@ -83,7 +83,7 @@ class SpelExpressionTransformer implements AggregationExpressionTransformer { * @param params must not be {@literal null} * @return */ - public Object transform(String expression, AggregationOperationContext context, Object... params) { + public @Nullable Object transform(String expression, AggregationOperationContext context, Object... params) { Assert.notNull(expression, "Expression must not be null"); Assert.notNull(context, "AggregationOperationContext must not be null"); @@ -96,7 +96,7 @@ public Object transform(String expression, AggregationOperationContext context, return transform(new AggregationExpressionTransformationContext<>(node, null, null, context)); } - public Object transform(AggregationExpressionTransformationContext context) { + public @Nullable Object transform(AggregationExpressionTransformationContext context) { return lookupConversionFor(context.getCurrentNode()).convert(context); } @@ -137,7 +137,7 @@ private static abstract class ExpressionNodeConversion * * @param transformer must not be {@literal null}. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "NullAway"}) public ExpressionNodeConversion(AggregationExpressionTransformer transformer) { Assert.notNull(transformer, "Transformer must not be null"); @@ -165,7 +165,7 @@ protected boolean supports(ExpressionNode node) { * @param context must not be {@literal null}. * @return */ - protected Object transform(ExpressionNode node, AggregationExpressionTransformationContext context) { + protected @Nullable Object transform(ExpressionNode node, AggregationExpressionTransformationContext context) { Assert.notNull(node, "ExpressionNode must not be null"); Assert.notNull(context, "AggregationExpressionTransformationContext must not be null"); @@ -183,7 +183,7 @@ protected Object transform(ExpressionNode node, AggregationExpressionTransformat * @param context must not be {@literal null}. * @return */ - protected Object transform(ExpressionNode node, @Nullable ExpressionNode parent, @Nullable Document operation, + protected @Nullable Object transform(ExpressionNode node, @Nullable ExpressionNode parent, @Nullable Document operation, AggregationExpressionTransformationContext context) { Assert.notNull(node, "ExpressionNode must not be null"); @@ -194,7 +194,7 @@ protected Object transform(ExpressionNode node, @Nullable ExpressionNode parent, } @Override - public Object transform(AggregationExpressionTransformationContext context) { + public @Nullable Object transform(AggregationExpressionTransformationContext context) { return transformer.transform(context); } @@ -204,7 +204,7 @@ public Object transform(AggregationExpressionTransformationContext context); + protected abstract @Nullable Object convert(AggregationExpressionTransformationContext context); } /** @@ -247,6 +247,7 @@ protected Object convert(AggregationExpressionTransformationContext context, OperatorNode currentNode) { @@ -301,7 +302,7 @@ private static class IndexerNodeConversion extends ExpressionNodeConversion context) { + protected @Nullable Object convert(AggregationExpressionTransformationContext context) { return context.addToPreviousOrReturn(context.getCurrentNode().getValue()); } @@ -322,9 +323,8 @@ private static class InlineListNodeConversion extends ExpressionNodeConversion context) { + protected @Nullable Object convert(AggregationExpressionTransformationContext context) { ExpressionNode currentNode = context.getCurrentNode(); @@ -355,7 +355,7 @@ private static class PropertyOrFieldReferenceNodeConversion extends ExpressionNo } @Override - protected Object convert(AggregationExpressionTransformationContext context) { + protected @Nullable Object convert(AggregationExpressionTransformationContext context) { String fieldReference = context.getFieldReference().toString(); return context.addToPreviousOrReturn(fieldReference); @@ -381,14 +381,14 @@ private static class LiteralNodeConversion extends ExpressionNodeConversion context) { + protected @Nullable Object convert(AggregationExpressionTransformationContext context) { LiteralNode node = context.getCurrentNode(); Object value = node.getValue(); if (context.hasPreviousOperation()) { - if (node.isUnaryMinus(context.getParentNode())) { + if (node.isUnaryMinus(context.getParentNode()) && value != null) { // unary minus operator return NumberUtils.convertNumberToTargetClass(((Number) value).doubleValue() * -1, (Class) value.getClass()); // retain type, e.g. int to -int @@ -419,7 +419,7 @@ private static class MethodReferenceNodeConversion extends ExpressionNodeConvers } @Override - protected Object convert(AggregationExpressionTransformationContext context) { + protected @Nullable Object convert(AggregationExpressionTransformationContext context) { MethodReferenceNode node = context.getCurrentNode(); AggregationMethodReference methodReference = node.getMethodReference(); @@ -469,7 +469,7 @@ private static class CompoundExpressionNodeConversion extends ExpressionNodeConv } @Override - protected Object convert(AggregationExpressionTransformationContext context) { + protected @Nullable Object convert(AggregationExpressionTransformationContext context) { ExpressionNode currentNode = context.getCurrentNode(); @@ -503,7 +503,7 @@ static class NotOperatorNodeConversion extends ExpressionNodeConversion context) { + protected @Nullable Object convert(AggregationExpressionTransformationContext context) { NotOperatorNode node = context.getCurrentNode(); List args = new ArrayList<>(); @@ -537,7 +537,7 @@ static class ValueRetrievingNodeConversion extends ExpressionNodeConversion context) { + protected @Nullable Object convert(AggregationExpressionTransformationContext context) { Object value = context.getCurrentNode().getValue(); return ObjectUtils.isArray(value) ? Arrays.asList(ObjectUtils.toObjectArray(value)) : value; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/StringOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/StringOperators.java index 9788497601..0f3447a476 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/StringOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/StringOperators.java @@ -21,8 +21,10 @@ import java.util.Map; import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Range; import org.springframework.data.mongodb.util.RegexFlags; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -60,8 +62,8 @@ public static StringOperatorFactory valueOf(AggregationExpression fieldReference */ public static class StringOperatorFactory { - private final String fieldReference; - private final AggregationExpression expression; + private final @Nullable String fieldReference; + private final @Nullable AggregationExpression expression; /** * Creates new {@link StringOperatorFactory} for given {@literal fieldReference}. @@ -126,6 +128,7 @@ public Concat concat(String value) { return createConcat().concat(value); } + @SuppressWarnings("NullAway") private Concat createConcat() { return usesFieldRef() ? Concat.valueOf(fieldReference) : Concat.valueOf(expression); } @@ -153,6 +156,7 @@ public Substr substring(int start, int nrOfChars) { return createSubstr().substring(start, nrOfChars); } + @SuppressWarnings("NullAway") private Substr createSubstr() { return usesFieldRef() ? Substr.valueOf(fieldReference) : Substr.valueOf(expression); } @@ -162,6 +166,7 @@ private Substr createSubstr() { * * @return new instance of {@link ToLower}. */ + @SuppressWarnings("NullAway") public ToLower toLower() { return usesFieldRef() ? ToLower.lowerValueOf(fieldReference) : ToLower.lowerValueOf(expression); } @@ -171,6 +176,7 @@ public ToLower toLower() { * * @return new instance of {@link ToUpper}. */ + @SuppressWarnings("NullAway") public ToUpper toUpper() { return usesFieldRef() ? ToUpper.upperValueOf(fieldReference) : ToUpper.upperValueOf(expression); } @@ -214,6 +220,7 @@ public StrCaseCmp strCaseCmpValueOf(AggregationExpression expression) { return createStrCaseCmp().strcasecmpValueOf(expression); } + @SuppressWarnings("NullAway") private StrCaseCmp createStrCaseCmp() { return usesFieldRef() ? StrCaseCmp.valueOf(fieldReference) : StrCaseCmp.valueOf(expression); } @@ -260,6 +267,7 @@ public IndexOfBytes indexOf(AggregationExpression expression) { return createIndexOfBytesSubstringBuilder().indexOf(expression); } + @SuppressWarnings("NullAway") private IndexOfBytes.SubstringBuilder createIndexOfBytesSubstringBuilder() { return usesFieldRef() ? IndexOfBytes.valueOf(fieldReference) : IndexOfBytes.valueOf(expression); } @@ -306,6 +314,7 @@ public IndexOfCP indexOfCP(AggregationExpression expression) { return createIndexOfCPSubstringBuilder().indexOf(expression); } + @SuppressWarnings("NullAway") private IndexOfCP.SubstringBuilder createIndexOfCPSubstringBuilder() { return usesFieldRef() ? IndexOfCP.valueOf(fieldReference) : IndexOfCP.valueOf(expression); } @@ -343,6 +352,7 @@ public Split split(AggregationExpression expression) { return createSplit().split(expression); } + @SuppressWarnings("NullAway") private Split createSplit() { return usesFieldRef() ? Split.valueOf(fieldReference) : Split.valueOf(expression); } @@ -353,6 +363,7 @@ private Split createSplit() { * * @return new instance of {@link StrLenBytes}. */ + @SuppressWarnings("NullAway") public StrLenBytes length() { return usesFieldRef() ? StrLenBytes.stringLengthOf(fieldReference) : StrLenBytes.stringLengthOf(expression); } @@ -363,6 +374,7 @@ public StrLenBytes length() { * * @return new instance of {@link StrLenCP}. */ + @SuppressWarnings("NullAway") public StrLenCP lengthCP() { return usesFieldRef() ? StrLenCP.stringLengthOfCP(fieldReference) : StrLenCP.stringLengthOfCP(expression); } @@ -390,6 +402,7 @@ public SubstrCP substringCP(int codePointStart, int nrOfCodePoints) { return createSubstrCP().substringCP(codePointStart, nrOfCodePoints); } + @SuppressWarnings("NullAway") private SubstrCP createSubstrCP() { return usesFieldRef() ? SubstrCP.valueOf(fieldReference) : SubstrCP.valueOf(expression); } @@ -432,6 +445,7 @@ public Trim trim(AggregationExpression expression) { return trim().charsOf(expression); } + @SuppressWarnings("NullAway") private Trim createTrim() { return usesFieldRef() ? Trim.valueOf(fieldReference) : Trim.valueOf(expression); } @@ -474,6 +488,7 @@ public LTrim ltrim(AggregationExpression expression) { return ltrim().charsOf(expression); } + @SuppressWarnings("NullAway") private LTrim createLTrim() { return usesFieldRef() ? LTrim.valueOf(fieldReference) : LTrim.valueOf(expression); } @@ -516,6 +531,7 @@ public RTrim rtrim(AggregationExpression expression) { return rtrim().charsOf(expression); } + @SuppressWarnings("NullAway") private RTrim createRTrim() { return usesFieldRef() ? RTrim.valueOf(fieldReference) : RTrim.valueOf(expression); } @@ -572,6 +588,7 @@ public RegexFind regexFind(String regex, String options) { return createRegexFind().regex(regex).options(options); } + @SuppressWarnings("NullAway") private RegexFind createRegexFind() { return usesFieldRef() ? RegexFind.valueOf(fieldReference) : RegexFind.valueOf(expression); } @@ -628,6 +645,7 @@ public RegexFindAll regexFindAll(String regex, String options) { return createRegexFindAll().regex(regex).options(options); } + @SuppressWarnings("NullAway") private RegexFindAll createRegexFindAll() { return usesFieldRef() ? RegexFindAll.valueOf(fieldReference) : RegexFindAll.valueOf(expression); } @@ -683,6 +701,7 @@ public RegexMatch regexMatch(String regex, String options) { return createRegexMatch().regex(regex).options(options); } + @SuppressWarnings("NullAway") private RegexMatch createRegexMatch() { return usesFieldRef() ? RegexMatch.valueOf(fieldReference) : RegexMatch.valueOf(expression); } @@ -713,6 +732,7 @@ public ReplaceOne replaceOne(AggregationExpression search, String replacement) { return createReplaceOne().findValueOf(search).replacement(replacement); } + @SuppressWarnings("NullAway") private ReplaceOne createReplaceOne() { return usesFieldRef() ? ReplaceOne.valueOf(fieldReference) : ReplaceOne.valueOf(expression); } @@ -743,6 +763,7 @@ public ReplaceAll replaceAll(AggregationExpression search, String replacement) { return createReplaceAll().findValueOf(search).replacement(replacement); } + @SuppressWarnings("NullAway") private ReplaceAll createReplaceAll() { return usesFieldRef() ? ReplaceAll.valueOf(fieldReference) : ReplaceAll.valueOf(expression); } @@ -810,6 +831,7 @@ public static Concat stringValue(String value) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Concat}. */ + @Contract("_ -> new") public Concat concatValueOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -822,6 +844,7 @@ public Concat concatValueOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Concat}. */ + @Contract("_ -> new") public Concat concatValueOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -834,6 +857,7 @@ public Concat concatValueOf(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Concat}. */ + @Contract("_ -> new") public Concat concat(String value) { return new Concat(append(value)); } @@ -883,6 +907,7 @@ public static Substr valueOf(AggregationExpression expression) { * @param start start index (including) * @return new instance of {@link Substr}. */ + @Contract("_ -> new") public Substr substring(int start) { return substring(start, -1); } @@ -892,6 +917,7 @@ public Substr substring(int start) { * @param nrOfChars * @return new instance of {@link Substr}. */ + @Contract("_, _ -> new") public Substr substring(int start, int nrOfChars) { return new Substr(append(Arrays.asList(start, nrOfChars))); } @@ -1055,16 +1081,19 @@ public static StrCaseCmp stringValue(String value) { return new StrCaseCmp(Collections.singletonList(value)); } + @Contract("_ -> new") public StrCaseCmp strcasecmp(String value) { return new StrCaseCmp(append(value)); } + @Contract("_ -> new") public StrCaseCmp strcasecmpValueOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); return new StrCaseCmp(append(Fields.field(fieldReference))); } + @Contract("_ -> new") public StrCaseCmp strcasecmpValueOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1118,6 +1147,7 @@ public static SubstringBuilder valueOf(AggregationExpression expression) { * @param range must not be {@literal null}. * @return new instance of {@link IndexOfBytes}. */ + @Contract("_ -> new") public IndexOfBytes within(Range range) { return new IndexOfBytes(append(AggregationUtils.toRangeValues(range))); } @@ -1208,6 +1238,7 @@ public static SubstringBuilder valueOf(AggregationExpression expression) { * @param range must not be {@literal null}. * @return new instance of {@link IndexOfCP}. */ + @Contract("_ -> new") public IndexOfCP within(Range range) { return new IndexOfCP(append(AggregationUtils.toRangeValues(range))); } @@ -1298,6 +1329,7 @@ public static Split valueOf(AggregationExpression expression) { * @param delimiter must not be {@literal null}. * @return new instance of {@link Split}. */ + @Contract("_ -> new") public Split split(String delimiter) { Assert.notNull(delimiter, "Delimiter must not be null"); @@ -1310,6 +1342,7 @@ public Split split(String delimiter) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Split}. */ + @Contract("_ -> new") public Split split(Field fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1322,6 +1355,7 @@ public Split split(Field fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Split}. */ + @Contract("_ -> new") public Split split(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1447,10 +1481,12 @@ public static SubstrCP valueOf(AggregationExpression expression) { return new SubstrCP(Collections.singletonList(expression)); } + @Contract("_ -> new") public SubstrCP substringCP(int start) { return substringCP(start, -1); } + @Contract("_, _ -> new") public SubstrCP substringCP(int start, int nrOfChars) { return new SubstrCP(append(Arrays.asList(start, nrOfChars))); } @@ -1501,6 +1537,7 @@ public static Trim valueOf(AggregationExpression expression) { * @param chars must not be {@literal null}. * @return new instance of {@link Trim}. */ + @Contract("_ -> new") public Trim chars(String chars) { Assert.notNull(chars, "Chars must not be null"); @@ -1514,6 +1551,7 @@ public Trim chars(String chars) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Trim}. */ + @Contract("_ -> new") public Trim charsOf(String fieldReference) { return new Trim(append("chars", Fields.field(fieldReference))); } @@ -1525,6 +1563,7 @@ public Trim charsOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Trim}. */ + @Contract("_ -> new") public Trim charsOf(AggregationExpression expression) { return new Trim(append("chars", expression)); } @@ -1598,6 +1637,7 @@ public static LTrim valueOf(AggregationExpression expression) { * @param chars must not be {@literal null}. * @return new instance of {@link LTrim}. */ + @Contract("_ -> new") public LTrim chars(String chars) { Assert.notNull(chars, "Chars must not be null"); @@ -1611,6 +1651,7 @@ public LTrim chars(String chars) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link LTrim}. */ + @Contract("_ -> new") public LTrim charsOf(String fieldReference) { return new LTrim(append("chars", Fields.field(fieldReference))); } @@ -1622,6 +1663,7 @@ public LTrim charsOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link LTrim}. */ + @Contract("_ -> new") public LTrim charsOf(AggregationExpression expression) { return new LTrim(append("chars", expression)); } @@ -1677,6 +1719,7 @@ public static RTrim valueOf(AggregationExpression expression) { * @param chars must not be {@literal null}. * @return new instance of {@link RTrim}. */ + @Contract("_ -> new") public RTrim chars(String chars) { Assert.notNull(chars, "Chars must not be null"); @@ -1689,6 +1732,7 @@ public RTrim chars(String chars) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link RTrim}. */ + @Contract("_ -> new") public RTrim charsOf(String fieldReference) { return new RTrim(append("chars", Fields.field(fieldReference))); } @@ -1699,6 +1743,7 @@ public RTrim charsOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link RTrim}. */ + @Contract("_ -> new") public RTrim charsOf(AggregationExpression expression) { return new RTrim(append("chars", expression)); } @@ -1757,6 +1802,7 @@ public static RegexFind valueOf(AggregationExpression expression) { * @param options must not be {@literal null}. * @return new instance of {@link RegexFind}. */ + @Contract("_ -> new") public RegexFind options(String options) { Assert.notNull(options, "Options must not be null"); @@ -1771,6 +1817,7 @@ public RegexFind options(String options) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link RegexFind}. */ + @Contract("_ -> new") public RegexFind optionsOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1785,6 +1832,7 @@ public RegexFind optionsOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link RegexFind}. */ + @Contract("_ -> new") public RegexFind optionsOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1798,6 +1846,7 @@ public RegexFind optionsOf(AggregationExpression expression) { * @param regex must not be {@literal null}. * @return new instance of {@link RegexFind}. */ + @Contract("_ -> new") public RegexFind regex(String regex) { Assert.notNull(regex, "Regex must not be null"); @@ -1811,6 +1860,7 @@ public RegexFind regex(String regex) { * @param pattern must not be {@literal null}. * @return new instance of {@link RegexFind}. */ + @Contract("_ -> new") public RegexFind pattern(Pattern pattern) { Assert.notNull(pattern, "Pattern must not be null"); @@ -1827,6 +1877,7 @@ public RegexFind pattern(Pattern pattern) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link RegexFind}. */ + @Contract("_ -> new") public RegexFind regexOf(String fieldReference) { Assert.notNull(fieldReference, "fieldReference must not be null"); @@ -1840,6 +1891,7 @@ public RegexFind regexOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link RegexFind}. */ + @Contract("_ -> new") public RegexFind regexOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1899,6 +1951,7 @@ public static RegexFindAll valueOf(AggregationExpression expression) { * @param options must not be {@literal null}. * @return new instance of {@link RegexFindAll}. */ + @Contract("_ -> new") public RegexFindAll options(String options) { Assert.notNull(options, "Options must not be null"); @@ -1913,6 +1966,7 @@ public RegexFindAll options(String options) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link RegexFindAll}. */ + @Contract("_ -> new") public RegexFindAll optionsOf(String fieldReference) { Assert.notNull(fieldReference, "fieldReference must not be null"); @@ -1927,6 +1981,7 @@ public RegexFindAll optionsOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link RegexFindAll}. */ + @Contract("_ -> new") public RegexFindAll optionsOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1940,6 +1995,7 @@ public RegexFindAll optionsOf(AggregationExpression expression) { * @param pattern must not be {@literal null}. * @return new instance of {@link RegexFindAll}. */ + @Contract("_ -> new") public RegexFindAll pattern(Pattern pattern) { Assert.notNull(pattern, "Pattern must not be null"); @@ -1956,6 +2012,7 @@ public RegexFindAll pattern(Pattern pattern) { * @param regex must not be {@literal null}. * @return new instance of {@link RegexFindAll}. */ + @Contract("_ -> new") public RegexFindAll regex(String regex) { Assert.notNull(regex, "Regex must not be null"); @@ -1969,6 +2026,7 @@ public RegexFindAll regex(String regex) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link RegexFindAll}. */ + @Contract("_ -> new") public RegexFindAll regexOf(String fieldReference) { Assert.notNull(fieldReference, "fieldReference must not be null"); @@ -1982,6 +2040,7 @@ public RegexFindAll regexOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link RegexFindAll}. */ + @Contract("_ -> new") public RegexFindAll regexOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -2043,6 +2102,7 @@ public static RegexMatch valueOf(AggregationExpression expression) { * @param options must not be {@literal null}. * @return new instance of {@link RegexMatch}. */ + @Contract("_ -> new") public RegexMatch options(String options) { Assert.notNull(options, "Options must not be null"); @@ -2057,6 +2117,7 @@ public RegexMatch options(String options) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link RegexMatch}. */ + @Contract("_ -> new") public RegexMatch optionsOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -2071,6 +2132,7 @@ public RegexMatch optionsOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link RegexMatch}. */ + @Contract("_ -> new") public RegexMatch optionsOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -2084,6 +2146,7 @@ public RegexMatch optionsOf(AggregationExpression expression) { * @param pattern must not be {@literal null}. * @return new instance of {@link RegexMatch}. */ + @Contract("_ -> new") public RegexMatch pattern(Pattern pattern) { Assert.notNull(pattern, "Pattern must not be null"); @@ -2100,6 +2163,7 @@ public RegexMatch pattern(Pattern pattern) { * @param regex must not be {@literal null}. * @return new instance of {@link RegexMatch}. */ + @Contract("_ -> new") public RegexMatch regex(String regex) { Assert.notNull(regex, "Regex must not be null"); @@ -2113,6 +2177,7 @@ public RegexMatch regex(String regex) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link RegexMatch}. */ + @Contract("_ -> new") public RegexMatch regexOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -2126,6 +2191,7 @@ public RegexMatch regexOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link RegexMatch}. */ + @Contract("_ -> new") public RegexMatch regexOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -2201,6 +2267,7 @@ public static ReplaceOne valueOf(AggregationExpression expression) { * @param replacement must not be {@literal null}. * @return new instance of {@link ReplaceOne}. */ + @Contract("_ -> new") public ReplaceOne replacement(String replacement) { Assert.notNull(replacement, "Replacement must not be null"); @@ -2215,6 +2282,7 @@ public ReplaceOne replacement(String replacement) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link ReplaceOne}. */ + @Contract("_ -> new") public ReplaceOne replacementOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -2229,6 +2297,7 @@ public ReplaceOne replacementOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link ReplaceOne}. */ + @Contract("_ -> new") public ReplaceOne replacementOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -2242,6 +2311,7 @@ public ReplaceOne replacementOf(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link ReplaceOne}. */ + @Contract("_ -> new") public ReplaceOne find(String value) { Assert.notNull(value, "Search string must not be null"); @@ -2255,6 +2325,7 @@ public ReplaceOne find(String value) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link ReplaceOne}. */ + @Contract("_ -> new") public ReplaceOne findValueOf(String fieldReference) { Assert.notNull(fieldReference, "fieldReference must not be null"); @@ -2269,6 +2340,7 @@ public ReplaceOne findValueOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link ReplaceOne}. */ + @Contract("_ -> new") public ReplaceOne findValueOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -2344,6 +2416,7 @@ public static ReplaceAll valueOf(AggregationExpression expression) { * @param replacement must not be {@literal null}. * @return new instance of {@link ReplaceAll}. */ + @Contract("_ -> new") public ReplaceAll replacement(String replacement) { Assert.notNull(replacement, "Replacement must not be null"); @@ -2358,6 +2431,7 @@ public ReplaceAll replacement(String replacement) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link ReplaceAll}. */ + @Contract("_ -> new") public ReplaceAll replacementValueOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -2372,6 +2446,7 @@ public ReplaceAll replacementValueOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link ReplaceAll}. */ + @Contract("_ -> new") public ReplaceAll replacementValueOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -2385,6 +2460,7 @@ public ReplaceAll replacementValueOf(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link ReplaceAll}. */ + @Contract("_ -> new") public ReplaceAll find(String value) { Assert.notNull(value, "Search string must not be null"); @@ -2398,6 +2474,7 @@ public ReplaceAll find(String value) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link ReplaceAll}. */ + @Contract("_ -> new") public ReplaceAll findValueOf(String fieldReference) { Assert.notNull(fieldReference, "fieldReference must not be null"); @@ -2411,6 +2488,7 @@ public ReplaceAll findValueOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link ReplaceAll}. */ + @Contract("_ -> new") public ReplaceAll findValueOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SystemVariable.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SystemVariable.java index 1fcf87d2a0..cc0296c900 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SystemVariable.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SystemVariable.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.aggregation; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Describes the system variables available in MongoDB aggregation framework pipeline expressions. @@ -116,8 +116,7 @@ public String getTarget() { return toString(); } - @Nullable - static String variableNameFrom(@Nullable String fieldRef) { + static @Nullable String variableNameFrom(@Nullable String fieldRef) { if (fieldRef == null || !fieldRef.startsWith(PREFIX) || fieldRef.length() <= 2) { return null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java index f30ebf394b..d2d49abf78 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java @@ -22,7 +22,7 @@ import org.bson.Document; import org.bson.codecs.configuration.CodecRegistry; - +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.context.MappingContext; @@ -33,7 +33,6 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperation.java index 057ada12d5..c93c1bad9e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperation.java @@ -19,7 +19,7 @@ import java.util.List; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnsetOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnsetOperation.java index ff765c37f7..0bcc192ded 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnsetOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnsetOperation.java @@ -23,6 +23,7 @@ import org.bson.Document; import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -67,6 +68,7 @@ public static UnsetOperation unset(String... fields) { * @param fields must not be {@literal null}. * @return new instance of {@link UnsetOperation}. */ + @Contract("_ -> new") public UnsetOperation and(String... fields) { List target = new ArrayList<>(this.fields); @@ -80,6 +82,7 @@ public UnsetOperation and(String... fields) { * @param fields must not be {@literal null}. * @return new instance of {@link UnsetOperation}. */ + @Contract("_ -> new") public UnsetOperation and(Field... fields) { List target = new ArrayList<>(this.fields); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnwindOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnwindOperation.java index d59ae01b12..cc0552cd1c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnwindOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnwindOperation.java @@ -16,8 +16,9 @@ package org.springframework.data.mongodb.core.aggregation; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -201,6 +202,8 @@ public static PathBuilder newBuilder() { @Override public UnwindOperation preserveNullAndEmptyArrays() { + Assert.notNull(field, "Path needs to be set first"); + if (arrayIndex != null) { return new UnwindOperation(field, arrayIndex, true); } @@ -211,6 +214,8 @@ public UnwindOperation preserveNullAndEmptyArrays() { @Override public UnwindOperation skipNullAndEmptyArrays() { + Assert.notNull(field, "Path needs to be set first"); + if (arrayIndex != null) { return new UnwindOperation(field, arrayIndex, false); } @@ -219,6 +224,7 @@ public UnwindOperation skipNullAndEmptyArrays() { } @Override + @Contract("_ -> this") public EmptyArraysBuilder arrayIndex(String field) { Assert.hasText(field, "'ArrayIndex' must not be null or empty"); @@ -227,6 +233,7 @@ public EmptyArraysBuilder arrayIndex(String field) { } @Override + @Contract("-> this") public EmptyArraysBuilder noArrayIndex() { arrayIndex = null; @@ -234,6 +241,7 @@ public EmptyArraysBuilder noArrayIndex() { } @Override + @Contract("_ -> this") public UnwindOperationBuilder path(String path) { Assert.hasText(path, "'Path' must not be null or empty"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VariableOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VariableOperators.java index 8e676c72bc..b5a9ca0f21 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VariableOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VariableOperators.java @@ -22,8 +22,8 @@ import java.util.stream.Collectors; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let.ExpressionVariable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -223,8 +223,7 @@ public static class Let implements AggregationExpression { private final List vars; - @Nullable // - private final AggregationExpression expression; + private final @Nullable AggregationExpression expression; private Let(List vars, @Nullable AggregationExpression expression) { @@ -333,6 +332,7 @@ private Document getMappedVariable(ExpressionVariable var, AggregationOperationC return new Document(var.variableName, var.expression); } + @SuppressWarnings("NullAway") private Object getMappedIn(AggregationOperationContext context) { return expression.toDocument(new NestedDelegatingExpressionAggregationOperationContext(context, this.vars.stream().map(var -> Fields.field(var.variableName)).collect(Collectors.toList()))); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperation.java index bcc5fbd7bc..95f1c5b4d2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperation.java @@ -24,13 +24,14 @@ import org.bson.BinaryVector; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Limit; import org.springframework.data.domain.Vector; import org.springframework.data.mongodb.core.mapping.MongoVector; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -55,12 +56,12 @@ public class VectorSearchOperation implements AggregationOperation { private final @Nullable Integer numCandidates; private final QueryPaths path; private final Vector vector; - private final String score; - private final Consumer scoreCriteria; + private final @Nullable String score; + private final @Nullable Consumer scoreCriteria; private VectorSearchOperation(SearchType searchType, @Nullable CriteriaDefinition filter, String indexName, Limit limit, @Nullable Integer numCandidates, QueryPaths path, Vector vector, @Nullable String searchScore, - Consumer scoreCriteria) { + @Nullable Consumer scoreCriteria) { this.searchType = searchType; this.filter = filter; @@ -236,7 +237,10 @@ public Document toDocument(AggregationOperationContext context) { } $vectorSearch.append("index", indexName); - $vectorSearch.append("limit", limit.max()); + + if(limit.isLimited()) { // TODO: exception or pass it on? + $vectorSearch.append("limit", limit.max()); + } if (numCandidates != null) { $vectorSearch.append("numCandidates", numCandidates); @@ -296,9 +300,9 @@ public String getOperator() { */ private static class VectorSearchBuilder implements PathContributor, VectorContributor, LimitContributor { - String index; - QueryPath paths; - Vector vector; + @Nullable String index; + @Nullable QueryPath paths; + @Nullable Vector vector; PathContributor index(String index) { this.index = index; @@ -314,6 +318,11 @@ public VectorContributor path(String path) { @Override public VectorSearchOperation limit(Limit limit) { + + Assert.notNull(index, "Index must be set first"); + Assert.notNull(paths, "Path must be set first"); + Assert.notNull(vector, "Vector must be set first"); + return new VectorSearchOperation(index, QueryPaths.of(paths), limit, vector); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/package-info.java index 0e30b8b855..2769990ca6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/package-info.java @@ -3,6 +3,6 @@ * * @since 1.3 */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.aggregation; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/package-info.java index 3e08dc1014..9ada2014a0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/package-info.java @@ -1,6 +1,6 @@ /** * Core Spring Data MongoDB annotations not limited to a special use case (like Query,...). */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.annotation; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverter.java index 7a01677939..9b1c744be2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverter.java @@ -20,6 +20,7 @@ import org.bson.types.Code; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; @@ -31,7 +32,6 @@ import org.springframework.data.mongodb.core.convert.MongoConverters.ObjectIdToBigIntegerConverter; import org.springframework.data.mongodb.core.convert.MongoConverters.ObjectIdToStringConverter; import org.springframework.data.mongodb.core.convert.MongoConverters.StringToObjectIdConverter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefProxyHandler.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefProxyHandler.java index 40afbb8c10..3b4dd99d4e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefProxyHandler.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefProxyHandler.java @@ -15,8 +15,8 @@ */ package org.springframework.data.mongodb.core.convert; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.lang.Nullable; import com.mongodb.DBRef; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java index 0235694030..ee1f568494 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java @@ -18,10 +18,10 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import com.mongodb.DBRef; @@ -60,7 +60,7 @@ Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbR * @param id will never be {@literal null}. * @return new instance of {@link DBRef}. */ - default DBRef createDbRef(@Nullable org.springframework.data.mongodb.core.mapping.DBRef annotation, + default DBRef createDbRef(org.springframework.data.mongodb.core.mapping.@Nullable DBRef annotation, MongoPersistentEntity entity, Object id) { if (annotation != null && StringUtils.hasText(annotation.db())) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolverCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolverCallback.java index bf6b882375..fd80029118 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolverCallback.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolverCallback.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core.convert; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; /** @@ -31,5 +32,5 @@ public interface DbRefResolverCallback { * @param property will never be {@literal null}. * @return */ - Object resolve(MongoPersistentProperty property); + @Nullable Object resolve(MongoPersistentProperty property); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefProxyHandler.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefProxyHandler.java index 22b1ce7981..13c0198aa0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefProxyHandler.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefProxyHandler.java @@ -18,13 +18,12 @@ import java.util.function.Function; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.lang.Nullable; import com.mongodb.DBRef; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java index de66c3ea94..c72bc4b886 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java @@ -25,6 +25,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.MongoDatabaseUtils; @@ -32,8 +33,8 @@ import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import com.mongodb.DBRef; @@ -71,7 +72,7 @@ public DefaultDbRefResolver(MongoDatabaseFactory mongoDbFactory) { } @Override - public Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbRefResolverCallback callback, + public @Nullable Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbRefResolverCallback callback, DbRefProxyHandler handler) { Assert.notNull(property, "Property must not be null"); @@ -86,7 +87,7 @@ public Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbr } @Override - public Document fetch(DBRef dbRef) { + public @Nullable Document fetch(DBRef dbRef) { return getReferenceLoader().fetchOne( DocumentReferenceQuery.forSingleDocument(Filters.eq(FieldName.ID.name(), dbRef.getId())), ReferenceCollection.fromDBRef(dbRef)); @@ -171,7 +172,7 @@ private boolean isLazyDbRef(MongoPersistentProperty property) { private static Stream documentWithId(Object identifier, Collection documents) { return documents.stream() // - .filter(it -> it.get(BasicMongoPersistentProperty.ID_FIELD_NAME).equals(identifier)) // + .filter(it -> ObjectUtils.nullSafeEquals(it.get(BasicMongoPersistentProperty.ID_FIELD_NAME), identifier)) // .limit(1); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverCallback.java index 82e5c9d0eb..376e0dd8cd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverCallback.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverCallback.java @@ -17,6 +17,7 @@ import org.bson.Document; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; @@ -53,7 +54,7 @@ class DefaultDbRefResolverCallback implements DbRefResolverCallback { } @Override - public Object resolve(MongoPersistentProperty property) { + public @Nullable Object resolve(MongoPersistentProperty property) { return resolver.getValueInternal(property, surroundingObject, evaluator, path); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultMongoTypeMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultMongoTypeMapper.java index 2c2b52afd5..f5db41f006 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultMongoTypeMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultMongoTypeMapper.java @@ -23,6 +23,7 @@ import org.bson.Document; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.data.convert.CustomConversions; import org.springframework.data.convert.DefaultTypeMapper; import org.springframework.data.convert.SimpleTypeInformationMapper; @@ -32,7 +33,6 @@ import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import com.mongodb.BasicDBList; @@ -114,7 +114,7 @@ public DefaultMongoTypeMapper(@Nullable String typeKey, List accessor, - MappingContext, ?> mappingContext, + @Nullable MappingContext, ?> mappingContext, List mappers) { super(accessor, mappingContext, mappers); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java index a7b3d6f21f..4df7c02f91 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java @@ -20,6 +20,7 @@ import java.util.Collections; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.DocumentReference; @@ -63,7 +64,8 @@ public DefaultReferenceResolver(ReferenceLoader referenceLoader, PersistenceExce } @Override - public Object resolveReference(MongoPersistentProperty property, Object source, + @SuppressWarnings("NullAway") + public @Nullable Object resolveReference(MongoPersistentProperty property, Object source, ReferenceLookupDelegate referenceLookupDelegate, MongoEntityReader entityReader) { LookupFunction lookupFunction = (property.isCollectionLike() || property.isMap()) ? collectionLookupFunction @@ -84,6 +86,7 @@ public Object resolveReference(MongoPersistentProperty property, Object source, * @see DBRef#lazy() * @see DocumentReference#lazy() */ + @SuppressWarnings("NullAway") protected boolean isLazyReference(MongoPersistentProperty property) { if (property.isDocumentReference()) { @@ -106,6 +109,7 @@ LazyLoadingProxyFactory getProxyFactory() { return proxyFactory; } + @SuppressWarnings("NullAway") private Object createLazyLoadingProxy(MongoPersistentProperty property, Object source, ReferenceLookupDelegate referenceLookupDelegate, LookupFunction lookupFunction, MongoEntityReader entityReader) { return proxyFactory.createLazyLoadingProxy(property, diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java index c795add9c8..ff50dd5df3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java @@ -21,11 +21,11 @@ import org.bson.Document; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.mongodb.DBObject; @@ -119,8 +119,7 @@ public void put(MongoPersistentProperty prop, @Nullable Object value) { * @param property must not be {@literal null}. * @return can be {@literal null}. */ - @Nullable - public Object get(MongoPersistentProperty property) { + public @Nullable Object get(MongoPersistentProperty property) { return BsonUtils.resolveValue(document, getFieldName(property)); } @@ -131,8 +130,7 @@ public Object get(MongoPersistentProperty property) { * @param entity must not be {@literal null}. * @return */ - @Nullable - public Object getRawId(MongoPersistentEntity entity) { + public @Nullable Object getRawId(MongoPersistentEntity entity) { return entity.hasIdProperty() ? get(entity.getRequiredIdProperty()) : BsonUtils.get(document, FieldName.ID.name()); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java index 8429584a6f..e03d215088 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java @@ -70,6 +70,7 @@ class DocumentPointerFactory { this.cache = new WeakHashMap<>(); } + @SuppressWarnings("NullAway") DocumentPointer computePointer( MappingContext, MongoPersistentProperty> mappingContext, MongoPersistentProperty property, Object value, Class typeHint) { @@ -87,7 +88,7 @@ DocumentPointer computePointer( if (usesDefaultLookup(property)) { - MongoPersistentProperty idProperty = persistentEntity.getIdProperty(); + MongoPersistentProperty idProperty = persistentEntity.getRequiredIdProperty(); Object idValue = persistentEntity.getIdentifierAccessor(value).getIdentifier(); if (idProperty.hasExplicitWriteTarget() @@ -114,6 +115,7 @@ DocumentPointer computePointer( .getDocumentPointer(mappingContext, persistentEntity, propertyAccessor); } + @SuppressWarnings("NullAway") private boolean usesDefaultLookup(MongoPersistentProperty property) { if (property.isDocumentReference()) { @@ -216,9 +218,16 @@ Object updatePlaceholders(org.bson.Document source, org.bson.Document target, MongoPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(entry.getKey()); if (persistentProperty != null && persistentProperty.isEntity()) { - MongoPersistentEntity nestedEntity = mappingContext.getPersistentEntity(persistentProperty.getType()); - target.put(entry.getKey(), updatePlaceholders(document, new Document(), mappingContext, - nestedEntity, nestedEntity.getPropertyAccessor(propertyAccessor.getProperty(persistentProperty)))); + MongoPersistentEntity nestedEntity = mappingContext.getRequiredPersistentEntity(persistentProperty.getType()); + Object propertyValue = propertyAccessor.getProperty(persistentProperty); + + if(propertyValue == null) { + target.put(entry.getKey(), propertyValue); + } else { + PersistentPropertyAccessor nestedAccessor = nestedEntity.getPropertyAccessor(propertyValue); + target.put(entry.getKey(), updatePlaceholders(document, new Document(), mappingContext, + nestedEntity, nestedAccessor)); + } } else { target.put(entry.getKey(), updatePlaceholders((Document) entry.getValue(), new Document(), mappingContext, persistentEntity, propertyAccessor)); @@ -236,7 +245,7 @@ Object updatePlaceholders(org.bson.Document source, org.bson.Document target, String fieldName = entry.getKey().equals(FieldName.ID.name()) ? "id" : entry.getKey(); if (!fieldName.contains(".")) { - Object targetValue = propertyAccessor.getProperty(persistentEntity.getPersistentProperty(fieldName)); + Object targetValue = propertyAccessor.getProperty(persistentEntity.getRequiredPersistentProperty(fieldName)); target.put(attribute, targetValue); continue; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPropertyAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPropertyAccessor.java index ea5ce01b44..a41e17c0ec 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPropertyAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPropertyAccessor.java @@ -18,11 +18,11 @@ import java.util.Map; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.context.expression.MapAccessor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.PropertyAccessor; import org.springframework.expression.TypedValue; -import org.springframework.lang.Nullable; /** * {@link PropertyAccessor} to allow entity based field access to {@link Document}s. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentReferenceSource.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentReferenceSource.java index bf21781058..b1e894efe9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentReferenceSource.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentReferenceSource.java @@ -15,7 +15,8 @@ */ package org.springframework.data.mongodb.core.convert; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; /** * The source object to resolve document references upon. Encapsulates the actual source and the reference specific @@ -56,8 +57,7 @@ public Object getSelf() { * * @return can be {@literal null}. */ - @Nullable - public Object getTargetSource() { + public @Nullable Object getTargetSource() { return targetSource; } @@ -67,8 +67,7 @@ public Object getTargetSource() { * @param source * @return */ - @Nullable - static Object getTargetSource(Object source) { + static @Nullable Object getTargetSource(@Nullable Object source) { return source instanceof DocumentReferenceSource referenceSource ? referenceSource.getTargetSource() : source; } @@ -78,7 +77,8 @@ static Object getTargetSource(Object source) { * @param self * @return */ - static Object getSelf(Object self) { + @Contract("null -> null") + static @Nullable Object getSelf(@Nullable Object self) { return self instanceof DocumentReferenceSource referenceSource ? referenceSource.getSelf() : self; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java index 2bca260b79..b595ab688f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java @@ -25,6 +25,7 @@ import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.WritingConverter; @@ -45,6 +46,7 @@ import org.springframework.data.mongodb.core.geo.GeoJsonPolygon; import org.springframework.data.mongodb.core.geo.Sphere; import org.springframework.data.mongodb.core.query.GeoCommand; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.NumberUtils; import org.springframework.util.ObjectUtils; @@ -130,7 +132,8 @@ enum DocumentToPointConverter implements Converter { INSTANCE; @Override - public Point convert(Document source) { + @Contract("null -> null; !null -> !null") + public @Nullable Point convert(@Nullable Document source) { if (source == null) { return null; @@ -157,7 +160,8 @@ enum PointToDocumentConverter implements Converter { INSTANCE; @Override - public Document convert(Point source) { + @Contract("null -> null; !null -> !null") + public @Nullable Document convert(@Nullable Point source) { return source == null ? null : new Document("x", source.getX()).append("y", source.getY()); } } @@ -174,7 +178,8 @@ enum BoxToDocumentConverter implements Converter { INSTANCE; @Override - public Document convert(Box source) { + @Contract("null -> null; !null -> !null") + public @Nullable Document convert(@Nullable Box source) { if (source == null) { return null; @@ -199,7 +204,9 @@ enum DocumentToBoxConverter implements Converter { INSTANCE; @Override - public Box convert(Document source) { + @Contract("null -> null; !null -> !null") + @SuppressWarnings("NullAway") + public @Nullable Box convert(@Nullable Document source) { if (source == null) { return null; @@ -223,7 +230,8 @@ enum CircleToDocumentConverter implements Converter { INSTANCE; @Override - public Document convert(Circle source) { + @Contract("null -> null; !null -> !null") + public @Nullable Document convert(@Nullable Circle source) { if (source == null) { return null; @@ -249,7 +257,8 @@ enum DocumentToCircleConverter implements Converter { INSTANCE; @Override - public Circle convert(Document source) { + @Contract("null -> null; !null -> !null") + public @Nullable Circle convert(@Nullable Document source) { if (source == null) { return null; @@ -261,7 +270,7 @@ public Circle convert(Document source) { Assert.notNull(center, "Center must not be null"); Assert.notNull(radius, "Radius must not be null"); - Distance distance = new Distance(toPrimitiveDoubleValue(radius)); + Distance distance = Distance.of(toPrimitiveDoubleValue(radius)); if (source.containsKey("metric")) { @@ -286,7 +295,8 @@ enum SphereToDocumentConverter implements Converter { INSTANCE; @Override - public Document convert(Sphere source) { + @Contract("null -> null; !null -> !null") + public @Nullable Document convert(@Nullable Sphere source) { if (source == null) { return null; @@ -312,7 +322,8 @@ enum DocumentToSphereConverter implements Converter { INSTANCE; @Override - public Sphere convert(Document source) { + @Contract("null -> null; !null -> !null") + public @Nullable Sphere convert(@Nullable Document source) { if (source == null) { return null; @@ -324,7 +335,7 @@ public Sphere convert(Document source) { Assert.notNull(center, "Center must not be null"); Assert.notNull(radius, "Radius must not be null"); - Distance distance = new Distance(toPrimitiveDoubleValue(radius)); + Distance distance = Distance.of(toPrimitiveDoubleValue(radius)); if (source.containsKey("metric")) { @@ -349,7 +360,8 @@ enum PolygonToDocumentConverter implements Converter { INSTANCE; @Override - public Document convert(Polygon source) { + @Contract("null -> null; !null -> !null") + public @Nullable Document convert(@Nullable Polygon source) { if (source == null) { return null; @@ -381,18 +393,20 @@ enum DocumentToPolygonConverter implements Converter { @Override @SuppressWarnings({ "unchecked" }) - public Polygon convert(Document source) { + @Contract("null -> null; !null -> !null") + public @Nullable Polygon convert(@Nullable Document source) { if (source == null) { return null; } List points = (List) source.get("points"); - List newPoints = new ArrayList<>(points.size()); + Assert.notNull(points, "Points elements of polygon must not be null"); + List newPoints = new ArrayList<>(points.size()); for (Document element : points) { - Assert.notNull(element, "Point elements of polygon must not be null"); + Assert.notNull(element, "Point elements of polygon must not contain null"); newPoints.add(DocumentToPointConverter.INSTANCE.convert(element)); } @@ -412,7 +426,8 @@ enum GeoCommandToDocumentConverter implements Converter { @Override @SuppressWarnings("rawtypes") - public Document convert(GeoCommand source) { + @Contract("null -> null; !null -> !null") + public @Nullable Document convert(@Nullable GeoCommand source) { if (source == null) { return null; @@ -463,7 +478,8 @@ enum GeoJsonToDocumentConverter implements Converter, Document> { INSTANCE; @Override - public Document convert(GeoJson source) { + @Contract("null -> null; !null -> !null") + public @Nullable Document convert(@Nullable GeoJson source) { if (source == null) { return null; @@ -490,7 +506,7 @@ public Document convert(GeoJson source) { private Object convertIfNecessary(Object candidate) { - if (candidate instanceof GeoJson geoJson) { + if (candidate instanceof GeoJson geoJson) { return convertIfNecessary(geoJson.getCoordinates()); } @@ -551,7 +567,8 @@ enum DocumentToGeoJsonPointConverter implements Converter null; !null -> !null") + public @Nullable GeoJsonPoint convert(@Nullable Document source) { if (source == null) { return null; @@ -560,7 +577,10 @@ public GeoJsonPoint convert(Document source) { Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "Point"), String.format("Cannot convert type '%s' to Point", source.get("type"))); - List dbl = (List) source.get("coordinates"); + if(!(source.get("coordinates") instanceof List sourceCoordinates)) { + throw new IllegalArgumentException("Coordinates need to be present"); + } + List dbl = (List) sourceCoordinates; return new GeoJsonPoint(toPrimitiveDoubleValue(dbl.get(0)), toPrimitiveDoubleValue(dbl.get(1))); } } @@ -574,7 +594,8 @@ enum DocumentToGeoJsonPolygonConverter implements Converter null; !null -> !null") + public @Nullable GeoJsonPolygon convert(@Nullable Document source) { if (source == null) { return null; @@ -596,7 +617,8 @@ enum DocumentToGeoJsonMultiPolygonConverter implements Converter null; !null -> !null") + public @Nullable GeoJsonMultiPolygon convert(@Nullable Document source) { if (source == null) { return null; @@ -606,8 +628,9 @@ public GeoJsonMultiPolygon convert(Document source) { String.format("Cannot convert type '%s' to MultiPolygon", source.get("type"))); List dbl = (List) source.get("coordinates"); - List polygones = new ArrayList<>(); + Assert.notNull(dbl, "Source needs to contain coordinates"); + List polygones = new ArrayList<>(dbl.size()); for (Object polygon : dbl) { polygones.add(toGeoJsonPolygon((List) polygon)); } @@ -625,7 +648,8 @@ enum DocumentToGeoJsonLineStringConverter implements Converter null; !null -> !null") + public @Nullable GeoJsonLineString convert(@Nullable Document source) { if (source == null) { return null; @@ -649,7 +673,8 @@ enum DocumentToGeoJsonMultiPointConverter implements Converter null; !null -> !null") + public @Nullable GeoJsonMultiPoint convert(@Nullable Document source) { if (source == null) { return null; @@ -673,7 +698,8 @@ enum DocumentToGeoJsonMultiLineStringConverter implements Converter null; !null -> !null") + public @Nullable GeoJsonMultiLineString convert(@Nullable Document source) { if (source == null) { return null; @@ -682,10 +708,13 @@ public GeoJsonMultiLineString convert(Document source) { Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "MultiLineString"), String.format("Cannot convert type '%s' to MultiLineString", source.get("type"))); - List lines = new ArrayList<>(); - List cords = (List) source.get("coordinates"); + if(!(source.get("coordinates") instanceof List coordinates)) { + throw new IllegalArgumentException("coordinates need to be present"); + } + + List lines = new ArrayList<>(coordinates.size()); - for (Object line : cords) { + for (Object line : coordinates) { lines.add(new GeoJsonLineString(toListOfPoint((List) line))); } return new GeoJsonMultiLineString(lines); @@ -700,9 +729,9 @@ enum DocumentToGeoJsonGeometryCollectionConverter implements Converter null; !null -> !null") + public @Nullable GeoJsonGeometryCollection convert(@Nullable Document source) { if (source == null) { return null; @@ -711,8 +740,12 @@ public GeoJsonGeometryCollection convert(Document source) { Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "GeometryCollection"), String.format("Cannot convert type '%s' to GeometryCollection", source.get("type"))); - List> geometries = new ArrayList<>(); - for (Object o : (List) source.get("geometries")) { + if(!(source.get("geometries") instanceof List sourceGeometries)) { + throw new IllegalArgumentException("Geometries need to be present"); + } + + List> geometries = new ArrayList<>(sourceGeometries.size()); + for (Object o : sourceGeometries) { geometries.add(toGenericGeoJson((Document) o)); } @@ -732,7 +765,10 @@ static List toList(Point point) { * @since 1.7 */ @SuppressWarnings("unchecked") - static List toListOfPoint(List listOfCoordinatePairs) { + @Contract("null -> fail") + static List toListOfPoint(@Nullable List listOfCoordinatePairs) { + + Assert.notNull(listOfCoordinatePairs, "ListOfCoordinatePairs must not be null"); List points = new ArrayList<>(listOfCoordinatePairs.size()); @@ -755,7 +791,10 @@ static List toListOfPoint(List listOfCoordinatePairs) { * @return never {@literal null}. * @since 1.7 */ - static GeoJsonPolygon toGeoJsonPolygon(List dbList) { + @Contract("null -> fail") + static GeoJsonPolygon toGeoJsonPolygon(@Nullable List dbList) { + + Assert.notNull(dbList, "DbList must not be null"); GeoJsonPolygon polygon = new GeoJsonPolygon(toListOfPoint((List) dbList.get(0))); return dbList.size() > 1 ? polygon.withInnerRing(toListOfPoint((List) dbList.get(1))) : polygon; @@ -794,7 +833,8 @@ private static GeoJson toGenericGeoJson(Document source) { throw new IllegalArgumentException(String.format("No converter found capable of converting GeoJson type %s", type)); } - private static double toPrimitiveDoubleValue(Object value) { + @Contract("null -> fail") + private static double toPrimitiveDoubleValue(@Nullable Object value) { Assert.isInstanceOf(Number.class, value, "Argument must be a Number"); return NumberUtils.convertNumberToTargetClass((Number) value, Double.class); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java index 77aac55813..6329d74d4f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.convert; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import com.mongodb.DBRef; @@ -53,8 +53,7 @@ public interface LazyLoadingProxy { * @return can be {@literal null}. * @since 3.3 */ - @Nullable - default Object getSource() { + default @Nullable Object getSource() { return toDBRef(); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyFactory.java index 76539ea431..eff58e7bd4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyFactory.java @@ -30,6 +30,8 @@ import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.ProxyFactory; import org.springframework.cglib.core.SpringNamingPolicy; import org.springframework.cglib.proxy.Callback; @@ -43,7 +45,6 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.Lock; import org.springframework.data.util.Lock.AcquiredLock; -import org.springframework.lang.Nullable; import org.springframework.objenesis.SpringObjenesis; import org.springframework.util.ReflectionUtils; @@ -124,7 +125,7 @@ private ProxyFactory prepareProxyFactory(Class propertyType, Supplier propertyType = property.getType(); LazyLoadingInterceptor interceptor = new LazyLoadingInterceptor(property, callback, source, exceptionTranslator); @@ -160,6 +161,7 @@ private Class getEnhancedTypeFor(Class type) { return enhancer.createClass(); } + @NullUnmarked public static class LazyLoadingInterceptor implements MethodInterceptor, org.springframework.cglib.proxy.MethodInterceptor, Serializable { @@ -180,10 +182,10 @@ public static class LazyLoadingInterceptor private final Lock readLock = Lock.of(rwLock.readLock()); private final Lock writeLock = Lock.of(rwLock.writeLock()); - private final MongoPersistentProperty property; - private final DbRefResolverCallback callback; - private final Object source; - private final PersistenceExceptionTranslator exceptionTranslator; + private final @Nullable MongoPersistentProperty property; + private final @Nullable DbRefResolverCallback callback; + private final @Nullable Object source; + private final @Nullable PersistenceExceptionTranslator exceptionTranslator; private volatile boolean resolved; private @Nullable Object result; @@ -191,18 +193,17 @@ public static class LazyLoadingInterceptor * @return a {@link LazyLoadingInterceptor} that just continues with the invocation. * @since 4.0 */ + @SuppressWarnings("NullAway") public static LazyLoadingInterceptor none() { return new LazyLoadingInterceptor(null, null, null, null) { - @Nullable @Override - public Object invoke(MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { return intercept(invocation.getThis(), invocation.getMethod(), invocation.getArguments(), null); } - @Nullable @Override - public Object intercept(Object o, Method method, Object[] args, MethodProxy proxy) throws Throwable { + public @Nullable Object intercept(Object o, Method method, @Nullable Object @Nullable[] args, @Nullable MethodProxy proxy) throws Throwable { ReflectionUtils.makeAccessible(method); return method.invoke(o, args); @@ -210,8 +211,8 @@ public Object intercept(Object o, Method method, Object[] args, MethodProxy prox }; } - public LazyLoadingInterceptor(MongoPersistentProperty property, DbRefResolverCallback callback, Object source, - PersistenceExceptionTranslator exceptionTranslator) { + public LazyLoadingInterceptor(@Nullable MongoPersistentProperty property, @Nullable DbRefResolverCallback callback, @Nullable Object source, + @Nullable PersistenceExceptionTranslator exceptionTranslator) { this.property = property; this.callback = callback; @@ -219,15 +220,13 @@ public LazyLoadingInterceptor(MongoPersistentProperty property, DbRefResolverCal this.exceptionTranslator = exceptionTranslator; } - @Nullable @Override - public Object invoke(MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { return intercept(invocation.getThis(), invocation.getMethod(), invocation.getArguments(), null); } - @Nullable @Override - public Object intercept(Object o, Method method, Object[] args, MethodProxy proxy) throws Throwable { + public @Nullable Object intercept(Object o, Method method, @Nullable Object @Nullable[] args, @Nullable MethodProxy proxy) throws Throwable { if (INITIALIZE_METHOD.equals(method)) { return ensureResolved(); @@ -247,7 +246,7 @@ public Object intercept(Object o, Method method, Object[] args, MethodProxy prox return proxyToString(source); } - if (ReflectionUtils.isEqualsMethod(method)) { + if (ReflectionUtils.isEqualsMethod(method) && args != null) { return proxyEquals(o, args[0]); } @@ -347,8 +346,8 @@ private void readObject(ObjectInputStream in) throws IOException { } } - @Nullable - private Object resolve() { + @SuppressWarnings("NullAway") + private @Nullable Object resolve() { try (AcquiredLock l = readLock.lock()) { if (resolved) { @@ -370,7 +369,7 @@ private Object resolve() { return writeLock.execute(() -> callback.resolve(property)); } catch (RuntimeException ex) { - DataAccessException translatedException = exceptionTranslator.translateExceptionIfPossible(ex); + DataAccessException translatedException = exceptionTranslator != null ? exceptionTranslator.translateExceptionIfPossible(ex) : null; if (translatedException instanceof ClientSessionException) { throw new LazyLoadingException("Unable to lazily resolve DBRef; Invalid session state", ex); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index 864cc1c3e3..24c3c2f590 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -39,12 +39,13 @@ import org.bson.conversions.Bson; import org.bson.json.JsonReader; import org.bson.types.ObjectId; - +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.CollectionFactory; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.support.DefaultConversionService; @@ -53,6 +54,7 @@ import org.springframework.core.env.StandardEnvironment; import org.springframework.data.annotation.Reference; import org.springframework.data.convert.CustomConversions; +import org.springframework.data.convert.PropertyValueConversions; import org.springframework.data.convert.PropertyValueConverter; import org.springframework.data.convert.TypeMapper; import org.springframework.data.convert.ValueConversionContext; @@ -71,7 +73,6 @@ import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider; import org.springframework.data.mapping.model.PropertyValueProvider; import org.springframework.data.mapping.model.SpELContext; -import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.mapping.model.ValueExpressionParameterValueProvider; import org.springframework.data.mongodb.CodecRegistryProvider; @@ -95,7 +96,7 @@ import org.springframework.data.util.Predicates; import org.springframework.data.util.TypeInformation; import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -158,7 +159,7 @@ public class MappingMongoConverter extends AbstractMongoConverter protected @Nullable ApplicationContext applicationContext; protected @Nullable Environment environment; - protected MongoTypeMapper typeMapper; + protected @Nullable MongoTypeMapper typeMapper; protected @Nullable String mapKeyDotReplacement = null; protected @Nullable CodecRegistryProvider codecRegistryProvider; @@ -308,7 +309,9 @@ public void setApplicationContext(ApplicationContext applicationContext) throws this.environment = applicationContext.getEnvironment(); this.spELContext = new SpELContext(this.spELContext, applicationContext); this.projectionFactory.setBeanFactory(applicationContext); - this.projectionFactory.setBeanClassLoader(applicationContext.getClassLoader()); + if(applicationContext.getClassLoader() != null) { + this.projectionFactory.setBeanClassLoader(applicationContext.getClassLoader()); + } if (entityCallbacks == null) { setEntityCallbacks(EntityCallbacks.create(applicationContext)); @@ -419,7 +422,7 @@ FieldName getFieldName(MongoPersistentProperty prop) { return accessor.getBean(); } - private Object doReadOrProject(ConversionContext context, Bson source, TypeInformation typeHint, + private Object doReadOrProject(ConversionContext context, @Nullable Bson source, TypeInformation typeHint, EntityProjection typeDescriptor) { if (typeDescriptor.isProjection()) { @@ -434,12 +437,12 @@ static class MapPersistentPropertyAccessor implements PersistentPropertyAccessor Map map = new LinkedHashMap<>(); @Override - public void setProperty(PersistentProperty persistentProperty, Object o) { + public void setProperty(PersistentProperty persistentProperty, @Nullable Object o) { map.put(persistentProperty.getName(), o); } @Override - public Object getProperty(PersistentProperty persistentProperty) { + public @Nullable Object getProperty(PersistentProperty persistentProperty) { return map.get(persistentProperty.getName()); } @@ -467,10 +470,14 @@ protected S read(TypeInformation type, Bson bson) { * @return the converted object, will never be {@literal null}. * @since 3.2 */ - @SuppressWarnings("unchecked") - protected S readDocument(ConversionContext context, Bson bson, + @SuppressWarnings({"unchecked","NullAway"}) + protected S readDocument(ConversionContext context, @Nullable Bson bson, TypeInformation typeHint) { + if(bson == null) { + bson = new Document(); + } + Document document = bson instanceof BasicDBObject dbObject ? new Document(dbObject) : (Document) bson; TypeInformation typeToRead = getTypeMapper().readType(document, typeHint); Class rawType = typeToRead.getType(); @@ -546,7 +553,7 @@ public EvaluatingDocumentAccessor(Bson document) { } @Override - public T evaluate(String expression) { + public @Nullable T evaluate(String expression) { return expressionEvaluatorFactory.create(getDocument()).evaluate(expression); } } @@ -600,8 +607,7 @@ private S populateProperties(ConversionContext context, MongoPersistentEntit * Reads the identifier from either the bean backing the {@link PersistentPropertyAccessor} or the source document in * case the identifier has not be populated yet. In this case the identifier is set on the bean for further reference. */ - @Nullable - private Object readAndPopulateIdentifier(ConversionContext context, PersistentPropertyAccessor accessor, + private @Nullable Object readAndPopulateIdentifier(ConversionContext context, PersistentPropertyAccessor accessor, DocumentAccessor document, MongoPersistentEntity entity, ValueExpressionEvaluator evaluator) { Object rawId = document.getRawId(entity); @@ -685,8 +691,7 @@ private DbRefResolverCallback getDbRefResolverCallback(ConversionContext context (prop, bson, e, path) -> MappingMongoConverter.this.getValueInternal(context, prop, bson, e)); } - @Nullable - private Object readAssociation(Association association, DocumentAccessor documentAccessor, + private @Nullable Object readAssociation(Association association, DocumentAccessor documentAccessor, DbRefProxyHandler handler, DbRefResolverCallback callback, ConversionContext context) { MongoPersistentProperty property = association.getInverse(); @@ -746,8 +751,8 @@ && peek(collection) instanceof Document) { } } - @Nullable - private Object readUnwrapped(ConversionContext context, DocumentAccessor documentAccessor, + @SuppressWarnings("NullAway") + private @Nullable Object readUnwrapped(ConversionContext context, DocumentAccessor documentAccessor, MongoPersistentProperty prop, MongoPersistentEntity unwrappedEntity) { if (prop.findAnnotation(Unwrapped.class).onEmpty().equals(OnEmpty.USE_EMPTY)) { @@ -763,13 +768,14 @@ private Object readUnwrapped(ConversionContext context, DocumentAccessor documen } @Override + @SuppressWarnings("NullAway") public DBRef toDBRef(Object object, @Nullable MongoPersistentProperty referringProperty) { org.springframework.data.mongodb.core.mapping.DBRef annotation; if (referringProperty != null) { annotation = referringProperty.getDBRef(); - Assert.isTrue(annotation != null, "The referenced property has to be mapped with @DBRef"); + Assert.notNull(annotation, "The referenced property has to be mapped with @DBRef"); } // DATAMONGO-913 @@ -781,6 +787,7 @@ public DBRef toDBRef(Object object, @Nullable MongoPersistentProperty referringP } @Override + @SuppressWarnings("NullAway") public DocumentPointer toDocumentPointer(Object source, @Nullable MongoPersistentProperty referringProperty) { if (source instanceof LazyLoadingProxy proxy) { @@ -800,6 +807,7 @@ public DocumentPointer toDocumentPointer(Object source, @Nullable MongoPersisten throw new IllegalArgumentException("The referringProperty is neither a DBRef nor a document reference"); } + @SuppressWarnings("NullAway") DocumentPointer createDocumentPointer(Object source, @Nullable MongoPersistentProperty referringProperty) { if (referringProperty == null) { @@ -864,7 +872,7 @@ private boolean requiresTypeHint(Class type) { /** * Internal write conversion method which should be used for nested invocations. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked","NullAway"}) protected void writeInternal(@Nullable Object obj, Bson bson, @Nullable TypeInformation typeHint) { if (null == obj) { @@ -1268,6 +1276,7 @@ protected String potentiallyEscapeMapKey(String source) { * * @param key */ + @SuppressWarnings("NullAway") private String potentiallyConvertMapKey(Object key) { if (key instanceof String stringValue) { @@ -1333,20 +1342,23 @@ private void writeSimpleInternal(@Nullable Object value, Bson bson, MongoPersist property.hasExplicitWriteTarget() ? property.getFieldType() : Object.class)); } - @Nullable @SuppressWarnings("unchecked") - private Object applyPropertyConversion(@Nullable Object value, MongoPersistentProperty property, + private @Nullable Object applyPropertyConversion(@Nullable Object value, MongoPersistentProperty property, PersistentPropertyAccessor persistentPropertyAccessor) { MongoConversionContext context = new MongoConversionContext(new PropertyValueProvider<>() { - @Nullable @Override - public T getPropertyValue(MongoPersistentProperty property) { + public @Nullable T getPropertyValue(MongoPersistentProperty property) { return (T) persistentPropertyAccessor.getProperty(property); } }, property, this, spELContext); - PropertyValueConverter> valueConverter = conversions - .getPropertyValueConversions().getValueConverter(property); + + PropertyValueConversions propertyValueConversions = conversions.getPropertyValueConversions(); + if(propertyValueConversions == null) { + return value; + } + + PropertyValueConverter> valueConverter = propertyValueConversions.getValueConverter(property); return value != null ? valueConverter.write(value, context) : valueConverter.writeNull(context); } @@ -1354,8 +1366,9 @@ public T getPropertyValue(MongoPersistentProperty property) { * Checks whether we have a custom conversion registered for the given value into an arbitrary simple Mongo type. * Returns the converted value if so. If not, we perform special enum handling or simply return the value as is. */ - @Nullable - private Object getPotentiallyConvertedSimpleWrite(@Nullable Object value, @Nullable Class typeHint) { + @Contract("null, _-> null") + @SuppressWarnings("NullAway") + private @Nullable Object getPotentiallyConvertedSimpleWrite(@Nullable Object value, @Nullable Class typeHint) { if (value == null) { return null; @@ -1391,7 +1404,7 @@ private Object getPotentiallyConvertedSimpleWrite(@Nullable Object value, @Nulla * * @since 3.2 */ - protected Object getPotentiallyConvertedSimpleRead(Object value, TypeInformation target) { + protected @Nullable Object getPotentiallyConvertedSimpleRead(@Nullable Object value, TypeInformation target) { return getPotentiallyConvertedSimpleRead(value, target.getType()); } @@ -1399,10 +1412,11 @@ protected Object getPotentiallyConvertedSimpleRead(Object value, TypeInformation * Checks whether we have a custom conversion for the given simple object. Converts the given value if so, applies * {@link Enum} handling or returns the value as is. */ + @Contract("null, _ -> null; _, null -> param1") @SuppressWarnings({ "rawtypes", "unchecked" }) - private Object getPotentiallyConvertedSimpleRead(Object value, @Nullable Class target) { + private @Nullable Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class target) { - if (target == null) { + if (target == null || value == null) { return value; } @@ -1421,6 +1435,7 @@ private Object getPotentiallyConvertedSimpleRead(Object value, @Nullable Class source, + @SuppressWarnings({"unchecked","NullAway"}) + protected @Nullable Object readCollectionOrArray(ConversionContext context, @Nullable Collection source, TypeInformation targetType) { Assert.notNull(targetType, "Target type must not be null"); + Assert.notNull(source, "Source must not be null"); Class collectionType = targetType.isSubTypeOf(Collection.class) // ? targetType.getType() // @@ -1518,7 +1533,7 @@ protected Object readCollectionOrArray(ConversionContext context, Collection * @return the converted {@link Map}, will never be {@literal null}. * @since 3.2 */ - protected Map readMap(ConversionContext context, Bson bson, TypeInformation targetType) { + protected @Nullable Map readMap(ConversionContext context, @Nullable Bson bson, TypeInformation targetType) { Assert.notNull(bson, "Document must not be null"); Assert.notNull(targetType, "TypeInformation must not be null"); @@ -1715,9 +1730,8 @@ private Object removeTypeInfo(Object object, boolean recursively) { return document; } - @Nullable @SuppressWarnings("unchecked") - T readValue(ConversionContext context, @Nullable Object value, TypeInformation type) { + @Nullable T readValue(ConversionContext context, @Nullable Object value, TypeInformation type) { if (value == null) { return null; @@ -1736,8 +1750,7 @@ T readValue(ConversionContext context, @Nullable Object value, TypeInformati return (T) context.convert(value, type); } - @Nullable - private Object readDBRef(ConversionContext context, @Nullable DBRef dbref, TypeInformation type) { + private @Nullable Object readDBRef(ConversionContext context, @Nullable DBRef dbref, TypeInformation type) { if (type.getType().equals(DBRef.class)) { return dbref; @@ -1802,6 +1815,7 @@ private List bulkReadAndConvertDBRefs(ConversionContext context, List event) { if (canPublishEvent()) { @@ -1881,12 +1895,12 @@ public MappingMongoConverter with(MongoDatabaseFactory dbFactory) { return target; } - private T doConvert(Object value, Class target) { + private @Nullable T doConvert(Object value, Class target) { return doConvert(value, target, null); } @SuppressWarnings("ConstantConditions") - private T doConvert(Object value, Class target, + private @Nullable T doConvert(Object value, Class target, @Nullable Class fallback) { if (conversionService.canConvert(value.getClass(), target) || fallback == null) { @@ -1940,7 +1954,7 @@ static class MongoDbPropertyValueProvider implements PropertyValueProvider T getPropertyValue(MongoPersistentProperty property) { + @SuppressWarnings({"unchecked", "NullAway"}) + public @Nullable T getPropertyValue(MongoPersistentProperty property) { String expression = property.getSpelExpression(); Object value = expression != null ? evaluator.evaluate(expression) : accessor.get(property); @@ -2059,7 +2072,7 @@ public T getPropertyValue(MongoPersistentProperty property) { } /** - * Extension of {@link SpELExpressionParameterValueProvider} to recursively trigger value conversion on the raw + * Extension of {@link ValueExpressionParameterValueProvider} to recursively trigger value conversion on the raw * resolved SpEL value. * * @author Oliver Gierke @@ -2110,7 +2123,7 @@ enum NoOpParameterValueProvider implements ParameterValueProvider T getParameterValue(Parameter parameter) { + public @Nullable T getParameterValue(Parameter parameter) { return null; } } @@ -2138,7 +2151,7 @@ public List> getParameterTypes( } @Override - public org.springframework.data.util.TypeInformation getProperty(String property) { + public org.springframework.data.util.@Nullable TypeInformation getProperty(String property) { return delegate.getProperty(property); } @@ -2173,7 +2186,7 @@ public TypeInformation getRawTypeInformation() { } @Override - public org.springframework.data.util.TypeInformation getActualType() { + public org.springframework.data.util.@Nullable TypeInformation getActualType() { return delegate.getActualType(); } @@ -2188,7 +2201,7 @@ public List> getParameterTypes( } @Override - public org.springframework.data.util.TypeInformation getSuperTypeInformation(Class superType) { + public org.springframework.data.util.@Nullable TypeInformation getSuperTypeInformation(Class superType) { return delegate.getSuperTypeInformation(superType); } @@ -2211,6 +2224,11 @@ public org.springframework.data.util.TypeInformation specialize(Typ public TypeDescriptor toTypeDescriptor() { return delegate.toTypeDescriptor(); } + + @Override + public ResolvableType toResolvableType() { + return delegate.toResolvableType(); + } } /** @@ -2280,8 +2298,7 @@ default ConversionContext forProperty(MongoPersistentProperty property) { * @return * @param */ - @Nullable - default S findContextualEntity(MongoPersistentEntity entity, Document document) { + default @Nullable S findContextualEntity(MongoPersistentEntity entity, Document document) { return null; } @@ -2315,7 +2332,7 @@ public ConversionContext withPath(ObjectPath currentPath) { } @Override - public S findContextualEntity(MongoPersistentEntity entity, Document document) { + public @Nullable S findContextualEntity(MongoPersistentEntity entity, Document document) { Object identifier = document.get(BasicMongoPersistentProperty.ID_FIELD_NAME); @@ -2352,15 +2369,15 @@ protected static class DefaultConversionContext implements ConversionContext { final ObjectPath path; final ContainerValueConverter documentConverter; final ContainerValueConverter> collectionConverter; - final ContainerValueConverter mapConverter; - final ContainerValueConverter dbRefConverter; - final ValueConverter elementConverter; + final ContainerValueConverter<@Nullable Bson> mapConverter; + final ContainerValueConverter<@Nullable DBRef> dbRefConverter; + final ValueConverter<@Nullable Object> elementConverter; DefaultConversionContext(MongoConverter sourceConverter, org.springframework.data.convert.CustomConversions customConversions, ObjectPath path, - ContainerValueConverter documentConverter, ContainerValueConverter> collectionConverter, - ContainerValueConverter mapConverter, ContainerValueConverter dbRefConverter, - ValueConverter elementConverter) { + ContainerValueConverter<@Nullable Bson> documentConverter, ContainerValueConverter<@Nullable Collection> collectionConverter, + ContainerValueConverter<@Nullable Bson> mapConverter, ContainerValueConverter<@Nullable DBRef> dbRefConverter, + ValueConverter<@Nullable Object> elementConverter) { this.sourceConverter = sourceConverter; this.conversions = customConversions; @@ -2372,8 +2389,8 @@ protected static class DefaultConversionContext implements ConversionContext { this.elementConverter = elementConverter; } - @SuppressWarnings("unchecked") @Override + @SuppressWarnings({"unchecked", "NullAway"}) public S convert(Object source, TypeInformation typeHint, ConversionContext context) { @@ -2454,7 +2471,7 @@ public ObjectPath getPath() { */ interface ValueConverter { - Object convert(T source, TypeInformation typeHint); + @Nullable Object convert(@Nullable T source, TypeInformation typeHint); } @@ -2466,7 +2483,7 @@ interface ValueConverter { */ interface ContainerValueConverter { - Object convert(ConversionContext context, T source, TypeInformation typeHint); + @Nullable Object convert(ConversionContext context, @Nullable T source, TypeInformation typeHint); } @@ -2481,7 +2498,7 @@ class ProjectingConversionContext extends DefaultConversionContext { ProjectingConversionContext(MongoConverter sourceConverter, CustomConversions customConversions, ObjectPath path, ContainerValueConverter> collectionConverter, ContainerValueConverter mapConverter, - ContainerValueConverter dbRefConverter, ValueConverter elementConverter, + ContainerValueConverter<@Nullable DBRef> dbRefConverter, ValueConverter<@Nullable Object> elementConverter, EntityProjection projection) { super(sourceConverter, customConversions, path, (context, source, typeHint) -> doReadOrProject(context, source, typeHint, projection), @@ -2533,7 +2550,7 @@ public void setProperty(PersistentProperty property, @Nullable Object value) } @Override - public Object getProperty(PersistentProperty property) { + public @Nullable Object getProperty(PersistentProperty property) { return delegate.getProperty(translate(property)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java index 5fde0acddd..0cc687c815 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java @@ -16,41 +16,56 @@ package org.springframework.data.mongodb.core.convert; import org.bson.conversions.Bson; - +import org.jspecify.annotations.Nullable; import org.springframework.data.convert.ValueConversionContext; import org.springframework.data.mapping.model.PropertyValueProvider; import org.springframework.data.mapping.model.SpELContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; +import org.springframework.lang.CheckReturnValue; /** * {@link ValueConversionContext} that allows to delegate read/write to an underlying {@link MongoConverter}. * * @author Christoph Strobl + * @author Ross Lawley * @since 3.4 */ public class MongoConversionContext implements ValueConversionContext { private final PropertyValueProvider accessor; // TODO: generics - private final @Nullable MongoPersistentProperty persistentProperty; private final MongoConverter mongoConverter; + @Nullable private final MongoPersistentProperty persistentProperty; @Nullable private final SpELContext spELContext; + @Nullable private final OperatorContext operatorContext; public MongoConversionContext(PropertyValueProvider accessor, @Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) { - this(accessor, persistentProperty, mongoConverter, null); + this(accessor, persistentProperty, mongoConverter, null, null); } public MongoConversionContext(PropertyValueProvider accessor, @Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter, @Nullable SpELContext spELContext) { + this(accessor, persistentProperty, mongoConverter, spELContext, null); + } + + public MongoConversionContext(PropertyValueProvider accessor, + @Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter, + @Nullable OperatorContext operatorContext) { + this(accessor, persistentProperty, mongoConverter, null, operatorContext); + } + + public MongoConversionContext(PropertyValueProvider accessor, + @Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter, + @Nullable SpELContext spELContext, @Nullable OperatorContext operatorContext) { this.accessor = accessor; this.persistentProperty = persistentProperty; this.mongoConverter = mongoConverter; this.spELContext = spELContext; + this.operatorContext = operatorContext; } @Override @@ -63,6 +78,16 @@ public MongoPersistentProperty getProperty() { return persistentProperty; } + /** + * @param operatorContext + * @return new instance of {@link MongoConversionContext}. + * @since 4.5 + */ + @CheckReturnValue + public MongoConversionContext forOperator(@Nullable OperatorContext operatorContext) { + return new MongoConversionContext(accessor, persistentProperty, mongoConverter, spELContext, operatorContext); + } + @Nullable public Object getValue(String propertyPath) { return accessor.getPropertyValue(getProperty().getOwner().getRequiredPersistentProperty(propertyPath)); @@ -70,12 +95,12 @@ public Object getValue(String propertyPath) { @Override @SuppressWarnings("unchecked") - public T write(@Nullable Object value, TypeInformation target) { + public @Nullable T write(@Nullable Object value, TypeInformation target) { return (T) mongoConverter.convertToMongoType(value, target); } @Override - public T read(@Nullable Object value, TypeInformation target) { + public @Nullable T read(@Nullable Object value, TypeInformation target) { return value instanceof Bson bson ? mongoConverter.read(target.getType(), bson) : ValueConversionContext.super.read(value, target); } @@ -84,4 +109,62 @@ public T read(@Nullable Object value, TypeInformation target) { public SpELContext getSpELContext() { return spELContext; } + + @Nullable + public OperatorContext getOperatorContext() { + return operatorContext; + } + + /** + * The {@link OperatorContext} provides access to the actual conversion intent like a write operation or a query + * operator such as {@literal $gte}. + * + * @since 4.5 + */ + public interface OperatorContext { + + /** + * The operator the conversion is used in. + * + * @return {@literal write} for simple write operations during save, or a query operator. + */ + String operator(); + + /** + * The context path the operator is used in. + * + * @return never {@literal null}. + */ + String path(); + + boolean isWriteOperation(); + + } + + record WriteOperatorContext(String path) implements OperatorContext { + + @Override + public String operator() { + return "write"; + } + + @Override + public boolean isWriteOperation() { + return true; + } + } + + record QueryOperatorContext(String operator, String path) implements OperatorContext { + + public QueryOperatorContext(@Nullable String operator, String path) { + this.operator = operator != null ? operator : "$eq"; + this.path = path; + } + + @Override + public boolean isWriteOperation() { + return false; + } + } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java index 3676e74c8b..e147d64cc5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java @@ -21,6 +21,7 @@ import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.ConversionException; import org.springframework.data.convert.CustomConversions; import org.springframework.data.convert.EntityConverter; @@ -33,7 +34,6 @@ import org.springframework.data.projection.EntityProjection; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -101,9 +101,8 @@ public interface MongoConverter * @throws IllegalArgumentException if {@literal targetType} is {@literal null}. * @since 2.1 */ - @SuppressWarnings("unchecked") - @Nullable - default T mapValueToTargetType(S source, Class targetType, DbRefResolver dbRefResolver) { + @SuppressWarnings({"unchecked","NullAway"}) + default @Nullable T mapValueToTargetType(S source, Class targetType, DbRefResolver dbRefResolver) { Assert.notNull(targetType, "TargetType must not be null"); Assert.notNull(dbRefResolver, "DbRefResolver must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java index f9a67d73a0..1fd45e1960 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java @@ -47,6 +47,7 @@ import org.bson.types.Code; import org.bson.types.Decimal128; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.TypeDescriptor; @@ -142,7 +143,7 @@ public String convert(ObjectId id) { enum StringToObjectIdConverter implements Converter { INSTANCE; - public ObjectId convert(String source) { + public @Nullable ObjectId convert(String source) { return StringUtils.hasText(source) ? new ObjectId(source) : null; } } @@ -206,7 +207,7 @@ public Decimal128 convert(BigInteger source) { enum StringToBigDecimalConverter implements Converter { INSTANCE; - public BigDecimal convert(String source) { + public @Nullable BigDecimal convert(String source) { return StringUtils.hasText(source) ? new BigDecimal(source) : null; } } @@ -235,7 +236,7 @@ public String convert(BigInteger source) { enum StringToBigIntegerConverter implements Converter { INSTANCE; - public BigInteger convert(String source) { + public @Nullable BigInteger convert(String source) { return StringUtils.hasText(source) ? new BigInteger(source) : null; } } @@ -312,20 +313,25 @@ public String convert(Term source) { * @author Christoph Strobl * @since 1.7 */ + @SuppressWarnings("NullAway") enum DocumentToNamedMongoScriptConverter implements Converter { INSTANCE; @Override - public NamedMongoScript convert(Document source) { + public @Nullable NamedMongoScript convert(Document source) { if (source.isEmpty()) { return null; } String id = source.get(FieldName.ID.name()).toString(); + Assert.notNull(id, "Script id must not be null"); + Object rawValue = source.get("value"); + Assert.isInstanceOf(Code.class, rawValue); + return new NamedMongoScript(id, ((Code) rawValue).getCode()); } } @@ -379,7 +385,7 @@ enum StringToCurrencyConverter implements Converter { INSTANCE; @Override - public Currency convert(String source) { + public @Nullable Currency convert(String source) { return StringUtils.hasText(source) ? Currency.getInstance(source) : null; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java index 050c3bd27d..8dccced380 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java @@ -32,6 +32,7 @@ import java.util.Set; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; @@ -51,7 +52,7 @@ import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigIntegerConverter; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -137,7 +138,7 @@ public Set getConvertibleTypes() { return new HashSet<>(Arrays.asList(localeToString, booleanToString)); } - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { return source != null ? source.toString() : null; } } @@ -188,6 +189,7 @@ public static MongoConverterConfigurationAdapter from(List converters) { * @param converter must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MongoConverterConfigurationAdapter registerConverter(Converter converter) { Assert.notNull(converter, "Converter must not be null"); @@ -202,6 +204,7 @@ public MongoConverterConfigurationAdapter registerConverter(Converter conv * @param converters must not be {@literal null} nor contain {@literal null} values. * @return this. */ + @Contract("_ -> this") public MongoConverterConfigurationAdapter registerConverters(Collection converters) { Assert.notNull(converters, "Converters must not be null"); @@ -217,6 +220,7 @@ public MongoConverterConfigurationAdapter registerConverters(Collection conve * @param converterFactory must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MongoConverterConfigurationAdapter registerConverterFactory(ConverterFactory converterFactory) { Assert.notNull(converterFactory, "ConverterFactory must not be null"); @@ -232,6 +236,7 @@ public MongoConverterConfigurationAdapter registerConverterFactory(ConverterFact * @return this. * @since 3.4 */ + @Contract("_ -> this") public MongoConverterConfigurationAdapter registerPropertyValueConverterFactory( PropertyValueConverterFactory converterFactory) { @@ -249,6 +254,7 @@ public MongoConverterConfigurationAdapter registerPropertyValueConverterFactory( * @return this. * @since 3.4 */ + @Contract("_ -> this") public MongoConverterConfigurationAdapter configurePropertyConversions( Consumer> configurationAdapter) { @@ -271,6 +277,7 @@ public MongoConverterConfigurationAdapter configurePropertyConversions( * @param useNativeDriverJavaTimeCodecs * @return this. */ + @Contract("_ -> this") public MongoConverterConfigurationAdapter useNativeDriverJavaTimeCodecs(boolean useNativeDriverJavaTimeCodecs) { this.useNativeDriverJavaTimeCodecs = useNativeDriverJavaTimeCodecs; @@ -285,6 +292,7 @@ public MongoConverterConfigurationAdapter useNativeDriverJavaTimeCodecs(boolean * @return this. * @see #useNativeDriverJavaTimeCodecs(boolean) */ + @Contract("-> this") public MongoConverterConfigurationAdapter useNativeDriverJavaTimeCodecs() { return useNativeDriverJavaTimeCodecs(true); } @@ -299,6 +307,7 @@ public MongoConverterConfigurationAdapter useNativeDriverJavaTimeCodecs() { * @return this. * @see #useNativeDriverJavaTimeCodecs(boolean) */ + @Contract("-> this") public MongoConverterConfigurationAdapter useSpringDataJavaTimeCodecs() { return useNativeDriverJavaTimeCodecs(false); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java index 0316251dc1..67f9d5ec46 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java @@ -28,6 +28,7 @@ import java.util.regex.Pattern; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher.NullHandler; import org.springframework.data.domain.ExampleMatcher.PropertyValueTransformer; @@ -98,13 +99,17 @@ public Document getMappedExample(Example example) { * @param entity must not be {@literal null}. * @return */ - public Document getMappedExample(Example example, MongoPersistentEntity entity) { + @SuppressWarnings("NullAway") + public Document getMappedExample(Example example, @Nullable MongoPersistentEntity entity) { Assert.notNull(example, "Example must not be null"); - Assert.notNull(entity, "MongoPersistentEntity must not be null"); Document reference = (Document) converter.convertToMongoType(example.getProbe()); + if(entity != null) { + entity = mappingContext.getRequiredPersistentEntity(example.getProbeType()); + } + if (entity.getIdProperty() != null && ClassUtils.isAssignable(entity.getType(), example.getProbeType())) { Object identifier = entity.getIdentifierAccessor(example.getProbe()).getIdentifier(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoJsonSchemaMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoJsonSchemaMapper.java index 8d199083e7..a5d329045e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoJsonSchemaMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoJsonSchemaMapper.java @@ -21,12 +21,12 @@ import java.util.stream.Collectors; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java index 867a6213d2..8aeb576c67 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java @@ -16,12 +16,12 @@ package org.springframework.data.mongodb.core.convert; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.data.convert.EntityWriter; import org.springframework.data.mongodb.core.mapping.DocumentPointer; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import com.mongodb.DBRef; @@ -43,8 +43,7 @@ public interface MongoWriter extends EntityWriter { * @param obj can be {@literal null}. * @return */ - @Nullable - default Object convertToMongoType(@Nullable Object obj) { + default @Nullable Object convertToMongoType(@Nullable Object obj) { return convertToMongoType(obj, (TypeInformation) null); } @@ -59,7 +58,7 @@ default Object convertToMongoType(@Nullable Object obj) { @Nullable Object convertToMongoType(@Nullable Object obj, @Nullable TypeInformation typeInformation); - default Object convertToMongoType(@Nullable Object obj, MongoPersistentEntity entity) { + default @Nullable Object convertToMongoType(@Nullable Object obj, MongoPersistentEntity entity) { return convertToMongoType(obj, entity.getTypeInformation()); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java index 265257af5c..68578f32b9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java @@ -18,9 +18,8 @@ import java.util.List; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.lang.Nullable; import com.mongodb.DBRef; @@ -37,16 +36,14 @@ public enum NoOpDbRefResolver implements DbRefResolver { INSTANCE; @Override - @Nullable - public Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbRefResolverCallback callback, + public @Nullable Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbRefResolverCallback callback, DbRefProxyHandler proxyHandler) { return handle(); } @Override - @Nullable - public Document fetch(DBRef dbRef) { + public @Nullable Document fetch(DBRef dbRef) { return handle(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ObjectPath.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ObjectPath.java index 5fefd472c4..d5f034eb1d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ObjectPath.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ObjectPath.java @@ -18,9 +18,9 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -99,8 +99,7 @@ ObjectPath push(Object object, MongoPersistentEntity entity, @Nullable Object * @return {@literal null} when no match found. * @since 2.0 */ - @Nullable - T getPathItem(Object id, String collection, Class type) { + @Nullable T getPathItem(Object id, String collection, Class type) { Assert.notNull(id, "Id must not be null"); Assert.hasText(collection, "Collection name must not be null"); @@ -133,13 +132,11 @@ Object getCurrentObject() { return getObject(); } - @Nullable - private Object getObject() { + private @Nullable Object getObject() { return object; } - @Nullable - private Object getIdValue() { + private @Nullable Object getIdValue() { return idValue; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index cce809adc6..11ed30aedd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java @@ -37,7 +37,8 @@ import org.bson.Document; import org.bson.conversions.Bson; import org.bson.types.ObjectId; - +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Reference; @@ -58,6 +59,8 @@ import org.springframework.data.mongodb.core.aggregation.AggregationExpression; import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext; import org.springframework.data.mongodb.core.convert.MappingMongoConverter.NestedDocument; +import org.springframework.data.mongodb.core.convert.MongoConversionContext.OperatorContext; +import org.springframework.data.mongodb.core.convert.MongoConversionContext.QueryOperatorContext; import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; @@ -66,7 +69,7 @@ import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.mongodb.util.DotPath; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -88,6 +91,7 @@ * @author David Julia * @author Divya Srivastava * @author Gyungrai Wang + * @author Ross Lawley */ public class QueryMapper { @@ -135,6 +139,7 @@ public Document getMappedObject(Bson query, Optional entity) { if (isNestedKeyword(query)) { @@ -275,6 +280,7 @@ public Document addMetaAttributes(Document source, @Nullable MongoPersistentEnti return mapMetaAttributes(source, entity, MetaMapping.FORCE); } + @SuppressWarnings("NullAway") private Document mapMetaAttributes(Document source, @Nullable MongoPersistentEntity entity, MetaMapping metaMapping) { @@ -347,7 +353,7 @@ private Document getMappedTextScoreField(MongoPersistentProperty property) { * @param rawValue * @return */ - protected Entry getMappedObjectForField(Field field, Object rawValue) { + protected Entry getMappedObjectForField(Field field, @Nullable Object rawValue) { String key = field.getMappedKey(); Object value; @@ -411,7 +417,9 @@ protected Document getMappedKeyword(Keyword keyword, @Nullable MongoPersistentEn } if (keyword.isSample()) { - return exampleMapper.getMappedExample(keyword.getValue(), entity); + + Example example = keyword.getValue(); + return exampleMapper.getMappedExample(example, entity != null ? entity : mappingContext.getRequiredPersistentEntity(example.getProbeType())); } if (keyword.isJsonSchema()) { @@ -453,8 +461,8 @@ protected Document getMappedKeyword(Field property, Keyword keyword) { * @return */ @Nullable - @SuppressWarnings("unchecked") - protected Object getMappedValue(Field documentField, Object sourceValue) { + @SuppressWarnings("NullAway") + protected Object getMappedValue(Field documentField, @Nullable Object sourceValue) { Object value = applyFieldTargetTypeHintToValue(documentField, sourceValue); @@ -491,6 +499,7 @@ private boolean isIdField(Field documentField) { && documentField.getProperty().getOwner().isIdProperty(documentField.getProperty()); } + @SuppressWarnings("NullAway") private Class getIdTypeForField(Field documentField) { return isIdField(documentField) ? documentField.getProperty().getFieldType() : ObjectId.class; } @@ -529,7 +538,7 @@ protected boolean isAssociationConversionNecessary(Field documentField, @Nullabl } MongoPersistentEntity entity = documentField.getPropertyEntity(); - return entity.hasIdProperty() + return entity != null && entity.hasIdProperty() && (type.equals(DBRef.class) || entity.getRequiredIdProperty().getActualType().isAssignableFrom(type)); } @@ -665,14 +674,31 @@ protected Object convertAssociation(@Nullable Object source, @Nullable MongoPers return createReferenceFor(source, property); } - @Nullable - private Object convertValue(Field documentField, Object sourceValue, Object value, + private @Nullable Object convertValue(Field documentField, @Nullable Object sourceValue, @Nullable Object value, PropertyValueConverter> valueConverter) { MongoPersistentProperty property = documentField.getProperty(); - MongoConversionContext conversionContext = new MongoConversionContext(NoPropertyPropertyValueProvider.INSTANCE, - property, converter); + OperatorContext criteriaContext = new QueryOperatorContext( + isKeyword(documentField.name) ? documentField.name : "$eq", property != null ? property.getFieldName() : documentField.name); + + MongoConversionContext conversionContext; + if (valueConverter instanceof MongoConversionContext mcc) { + conversionContext = mcc.forOperator(criteriaContext); + } else { + conversionContext = new MongoConversionContext(NoPropertyPropertyValueProvider.INSTANCE, property, converter, + criteriaContext); + } + + return convertValueWithConversionContext(documentField, sourceValue, value, valueConverter, conversionContext); + } + + @SuppressWarnings("NullAway") + protected @Nullable Object convertValueWithConversionContext(Field documentField, @Nullable Object sourceValue, @Nullable Object value, + PropertyValueConverter> valueConverter, + MongoConversionContext conversionContext) { + + MongoPersistentProperty property = documentField.getProperty(); /* might be an $in clause with multiple entries */ if (property != null && !property.isCollectionLike() && sourceValue instanceof Collection collection) { @@ -688,21 +714,22 @@ private Object convertValue(Field documentField, Object sourceValue, Object valu return converted; } - if (property != null && !documentField.getProperty().isMap() && sourceValue instanceof Document document) { + if (property != null && !property.isMap() && sourceValue instanceof Document document) { return BsonUtils.mapValues(document, (key, val) -> { if (isKeyword(key)) { - return getMappedValue(documentField, val); + return convertValueWithConversionContext(documentField, val, val, valueConverter, conversionContext + .forOperator(new QueryOperatorContext(key, conversionContext.getOperatorContext().path()))); } return val; }); } - return valueConverter.write(value, conversionContext); + return value != null ? valueConverter.write(value, conversionContext) : value; } @Nullable - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "NullAway"}) private Object convertIdField(Field documentField, Object source) { Object value = source; @@ -836,6 +863,7 @@ public Object convertId(@Nullable Object id, Class targetType) { * @param candidate * @return */ + @Contract("null -> false") protected boolean isNestedKeyword(@Nullable Object candidate) { if (!(candidate instanceof Document)) { @@ -870,8 +898,8 @@ protected boolean isTypeKey(String key) { * @param candidate * @return */ - protected boolean isKeyword(String candidate) { - return candidate.startsWith("$"); + protected boolean isKeyword(@Nullable String candidate) { + return candidate != null && candidate.startsWith("$"); } /** @@ -916,6 +944,7 @@ private Object applyFieldTargetTypeHintToValue(Field documentField, @Nullable Ob * @author Oliver Gierke * @author Christoph Strobl */ + @SuppressWarnings("NullAway") static class Keyword { private static final Set NON_DBREF_CONVERTING_KEYWORDS = Set.of("$", "$size", "$slice", "$gt", "$lt"); @@ -1164,6 +1193,7 @@ public MetadataBackedField(String name, MongoPersistentEntity entity, * @param context must not be {@literal null}. * @param property may be {@literal null}. */ + @SuppressWarnings("NullAway") public MetadataBackedField(String name, MongoPersistentEntity entity, MappingContext, MongoPersistentProperty> context, @Nullable MongoPersistentProperty property) { @@ -1207,7 +1237,7 @@ public MongoPersistentProperty getProperty() { } @Override - public MongoPersistentEntity getPropertyEntity() { + public @Nullable MongoPersistentEntity getPropertyEntity() { MongoPersistentProperty property = getProperty(); return property == null ? null : mappingContext.getPersistentEntity(property); } @@ -1224,7 +1254,7 @@ public boolean isAssociation() { } @Override - public Association getAssociation() { + public @Nullable Association getAssociation() { return association; } @@ -1427,6 +1457,7 @@ protected Converter getPropertyConverter() { * @return * @since 1.7 */ + @SuppressWarnings("NullAway") protected Converter getAssociationConverter() { return new AssociationConverter(name, getAssociation()); } @@ -1578,7 +1609,7 @@ public AssociationConverter(String name, Association as } @Override - public String convert(MongoPersistentProperty source) { + public @Nullable String convert(MongoPersistentProperty source) { if (associationFound) { return null; @@ -1606,7 +1637,7 @@ public MongoConverter getConverter() { return converter; } - private enum NoPropertyPropertyValueProvider implements PropertyValueProvider { + enum NoPropertyPropertyValueProvider implements PropertyValueProvider { INSTANCE; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java index 5a1adf9114..cd7d55311d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java @@ -20,9 +20,9 @@ import org.bson.Document; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceCollection; import org.springframework.data.mongodb.core.mapping.FieldName; -import org.springframework.lang.Nullable; import com.mongodb.client.MongoCollection; @@ -42,8 +42,7 @@ public interface ReferenceLoader { * @param context must not be {@literal null}. * @return the matching {@link Document} or {@literal null} if none found. */ - @Nullable - default Document fetchOne(DocumentReferenceQuery referenceQuery, ReferenceCollection context) { + default @Nullable Document fetchOne(DocumentReferenceQuery referenceQuery, ReferenceCollection context) { Iterator it = fetchMany(referenceQuery, context).iterator(); return it.hasNext() ? it.next() : null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java index b912cfb540..a0e5a6f2bb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java @@ -31,6 +31,7 @@ import org.bson.Document; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.SpELContext; import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery; @@ -47,7 +48,6 @@ import org.springframework.data.mongodb.util.spel.ExpressionUtils; import org.springframework.data.util.Streamable; import org.springframework.expression.EvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -98,8 +98,7 @@ public ReferenceLookupDelegate( * {@literal null}. * @return can be {@literal null}. */ - @Nullable - public Object readReference(MongoPersistentProperty property, Object source, LookupFunction lookupFunction, + public @Nullable Object readReference(MongoPersistentProperty property, Object source, LookupFunction lookupFunction, MongoEntityReader entityReader) { Object value = source instanceof DocumentReferenceSource documentReferenceSource @@ -126,7 +125,7 @@ public Object readReference(MongoPersistentProperty property, Object source, Loo @Nullable private Iterable retrieveRawDocuments(MongoPersistentProperty property, Object source, - LookupFunction lookupFunction, Object value) { + LookupFunction lookupFunction, @Nullable Object value) { DocumentReferenceQuery filter = computeFilter(property, source, spELContext); if (filter instanceof NoResultsFilter) { @@ -137,7 +136,8 @@ private Iterable retrieveRawDocuments(MongoPersistentProperty property return lookupFunction.apply(filter, referenceCollection); } - private ReferenceCollection computeReferenceContext(MongoPersistentProperty property, Object value, + @SuppressWarnings("NullAway") + private ReferenceCollection computeReferenceContext(MongoPersistentProperty property, @Nullable Object value, SpELContext spELContext) { // Use the first value as a reference for others in case of collection like @@ -195,7 +195,7 @@ private ReferenceCollection computeReferenceContext(MongoPersistentProperty prop * @return can be {@literal null}. */ @SuppressWarnings("unchecked") - private T parseValueOrGet(String value, ParameterBindingContext bindingContext, Supplier defaultValue) { + private T parseValueOrGet(String value, ParameterBindingContext bindingContext, Supplier<@Nullable T> defaultValue) { if (!StringUtils.hasText(value)) { return defaultValue.get(); @@ -220,7 +220,7 @@ private T parseValueOrGet(String value, ParameterBindingContext bindingConte return evaluated != null ? evaluated : defaultValue.get(); } - ParameterBindingContext bindingContext(MongoPersistentProperty property, Object source, SpELContext spELContext) { + ParameterBindingContext bindingContext(MongoPersistentProperty property, @Nullable Object source, SpELContext spELContext) { ValueProvider valueProvider = valueProviderFor(DocumentReferenceSource.getTargetSource(source)); @@ -228,7 +228,7 @@ ParameterBindingContext bindingContext(MongoPersistentProperty property, Object () -> evaluationContextFor(property, source, spELContext)); } - ValueProvider valueProviderFor(Object source) { + ValueProvider valueProviderFor(@Nullable Object source) { return index -> { if (source instanceof Document document) { @@ -238,7 +238,7 @@ ValueProvider valueProviderFor(Object source) { }; } - EvaluationContext evaluationContextFor(MongoPersistentProperty property, Object source, SpELContext spELContext) { + EvaluationContext evaluationContextFor(MongoPersistentProperty property, @Nullable Object source, SpELContext spELContext) { Object target = source instanceof DocumentReferenceSource documentReferenceSource ? documentReferenceSource.getTargetSource() @@ -264,7 +264,7 @@ EvaluationContext evaluationContextFor(MongoPersistentProperty property, Object * @param spELContext must not be {@literal null}. * @return never {@literal null}. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked","NullAway"}) DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object source, SpELContext spELContext) { DocumentReference documentReference = property.isDocumentReference() ? property.getDocumentReference() diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java index 715327d18e..0698b08bf8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java @@ -15,11 +15,11 @@ */ package org.springframework.data.mongodb.core.convert; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.mongodb.DBRef; @@ -54,8 +54,7 @@ Object resolveReference(MongoPersistentProperty property, Object source, */ class ReferenceCollection { - @Nullable // - private final String database; + private final @Nullable String database; private final String collection; /** @@ -95,8 +94,7 @@ public String getCollection() { * * @return can be {@literal null}. */ - @Nullable - public String getDatabase() { + public @Nullable String getDatabase() { return database; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java index 35cb578c23..bff72427f7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java @@ -23,18 +23,21 @@ import org.bson.Document; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.PropertyValueConverter; +import org.springframework.data.convert.ValueConversionContext; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mongodb.core.convert.MongoConversionContext.WriteOperatorContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update.Modifier; import org.springframework.data.mongodb.core.query.Update.Modifiers; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -129,7 +132,7 @@ public static boolean isUpdateObject(@Nullable Document updateObj) { * org.springframework.data.mongodb.core.mapping.MongoPersistentEntity) */ @Override - protected Object delegateConvertToMongoType(Object source, @Nullable MongoPersistentEntity entity) { + protected @Nullable Object delegateConvertToMongoType(Object source, @Nullable MongoPersistentEntity entity) { if (entity != null && entity.isUnwrapped()) { return converter.convertToMongoType(source, entity); @@ -140,7 +143,8 @@ protected Object delegateConvertToMongoType(Object source, @Nullable MongoPersis } @Override - protected Entry getMappedObjectForField(Field field, Object rawValue) { + @SuppressWarnings("NullAway") + protected Entry getMappedObjectForField(Field field, @Nullable Object rawValue) { if (isDocument(rawValue)) { @@ -160,6 +164,13 @@ protected Entry getMappedObjectForField(Field field, Object rawV return super.getMappedObjectForField(field, rawValue); } + protected @Nullable Object convertValueWithConversionContext(Field documentField, @Nullable Object sourceValue, @Nullable Object value, + PropertyValueConverter> valueConverter, + MongoConversionContext conversionContext) { + + return super.convertValueWithConversionContext(documentField, sourceValue, value, valueConverter, conversionContext.forOperator(new WriteOperatorContext(documentField.name))); + } + private Entry getMappedUpdateModifier(Field field, Object rawValue) { Object value; @@ -196,11 +207,12 @@ private boolean isQuery(@Nullable Object value) { return value instanceof Query; } - private Document getMappedValue(@Nullable Field field, Modifier modifier) { + private @Nullable Document getMappedValue(@Nullable Field field, Modifier modifier) { return new Document(modifier.getKey(), getMappedModifier(field, modifier)); } - private Object getMappedModifier(@Nullable Field field, Modifier modifier) { + @SuppressWarnings("NullAway") + private @Nullable Object getMappedModifier(@Nullable Field field, Modifier modifier) { Object value = modifier.getValue(); @@ -211,7 +223,7 @@ private Object getMappedModifier(@Nullable Field field, Modifier modifier) { : getMappedSort(sortObject, field.getPropertyEntity()); } - if (isAssociationConversionNecessary(field, value)) { + if (field != null && isAssociationConversionNecessary(field, value)) { if (ObjectUtils.isArray(value) || value instanceof Collection) { List targetPointers = new ArrayList<>(); for (Object val : converter.getConversionService().convert(value, List.class)) { @@ -229,7 +241,7 @@ private Object getMappedModifier(@Nullable Field field, Modifier modifier) { private TypeInformation getTypeHintForEntity(@Nullable Object source, MongoPersistentEntity entity) { TypeInformation info = entity.getTypeInformation(); - Class type = info.getActualType().getType(); + Class type = info.getRequiredActualType().getType(); if (source == null || type.isInterface() || java.lang.reflect.Modifier.isAbstract(type.getModifiers())) { return info; @@ -247,7 +259,7 @@ private TypeInformation getTypeHintForEntity(@Nullable Object source, MongoPe } @Override - protected Field createPropertyField(MongoPersistentEntity entity, String key, + protected Field createPropertyField(@Nullable MongoPersistentEntity entity, String key, MappingContext, MongoPersistentProperty> mappingContext) { return entity == null ? super.createPropertyField(entity, key, mappingContext) @@ -306,6 +318,7 @@ protected Converter getPropertyConverter() { } @Override + @SuppressWarnings("NullAway") protected Converter getAssociationConverter() { return new UpdateAssociationConverter(getMappingContext(), getAssociation(), key); } @@ -333,7 +346,7 @@ public UpdateAssociationConverter( } @Override - public String convert(MongoPersistentProperty source) { + public @Nullable String convert(MongoPersistentProperty source) { return super.convert(source) == null ? null : mapper.mapPropertyName(source); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ValueResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ValueResolver.java index 0a96cc867a..8eed053a09 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ValueResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ValueResolver.java @@ -17,10 +17,9 @@ import org.bson.Document; import org.bson.conversions.Bson; - +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.lang.Nullable; /** * Internal API to trigger the resolution of properties. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/EncryptingConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/EncryptingConverter.java index 4097be7704..b31d8a2b7c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/EncryptingConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/EncryptingConverter.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core.convert.encryption; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.convert.MongoConversionContext; import org.springframework.data.mongodb.core.convert.MongoValueConverter; import org.springframework.data.mongodb.core.encryption.EncryptionContext; @@ -28,7 +29,7 @@ public interface EncryptingConverter extends MongoValueConverter { @Override - default S read(Object value, MongoConversionContext context) { + default @Nullable S read(Object value, MongoConversionContext context) { return decrypt(value, buildEncryptionContext(context)); } @@ -39,10 +40,10 @@ default S read(Object value, MongoConversionContext context) { * @param context the context to operate in. * @return never {@literal null}. */ - S decrypt(Object encryptedValue, EncryptionContext context); + @Nullable S decrypt(Object encryptedValue, EncryptionContext context); @Override - default T write(Object value, MongoConversionContext context) { + default @Nullable T write(Object value, MongoConversionContext context) { return encrypt(value, buildEncryptionContext(context)); } @@ -53,7 +54,7 @@ default T write(Object value, MongoConversionContext context) { * @param context the context to operate in. * @return never {@literal null}. */ - T encrypt(Object value, EncryptionContext context); + T encrypt(@Nullable Object value, EncryptionContext context); /** * Obtain the {@link EncryptionContext} for a given {@link MongoConversionContext value conversion context}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java index f8d814fee4..0431cf11dd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java @@ -15,17 +15,19 @@ */ package org.springframework.data.mongodb.core.convert.encryption; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.convert.MongoConversionContext; +import org.springframework.data.mongodb.core.convert.MongoConversionContext.OperatorContext; import org.springframework.data.mongodb.core.encryption.EncryptionContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; import org.springframework.expression.EvaluationContext; -import org.springframework.lang.Nullable; /** * Default {@link EncryptionContext} implementation. * * @author Christoph Strobl + * @author Ross Lawley * @since 4.1 */ class ExplicitEncryptionContext implements EncryptionContext { @@ -41,29 +43,39 @@ public MongoPersistentProperty getProperty() { return conversionContext.getProperty(); } - @Nullable @Override - public Object lookupValue(String path) { + public @Nullable Object lookupValue(String path) { return conversionContext.getValue(path); } @Override - public Object convertToMongoType(Object value) { + public @Nullable Object convertToMongoType(Object value) { return conversionContext.write(value); } @Override - public EvaluationContext getEvaluationContext(Object source) { - return conversionContext.getSpELContext().getEvaluationContext(source); + public EvaluationContext getEvaluationContext(@Nullable Object source) { + + if(conversionContext.getSpELContext() != null) { + return conversionContext.getSpELContext().getEvaluationContext(source); + } + + throw new IllegalStateException("SpEL context not present"); } @Override - public T read(@Nullable Object value, TypeInformation target) { + public @Nullable T read(@Nullable Object value, TypeInformation target) { return conversionContext.read(value, target); } @Override - public T write(@Nullable Object value, TypeInformation target) { + public @Nullable T write(@Nullable Object value, TypeInformation target) { return conversionContext.write(value, target); } + + @Override + @Nullable + public OperatorContext getOperatorContext() { + return conversionContext.getOperatorContext(); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java index 1ce24b25fe..ecab645fe5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java @@ -15,8 +15,13 @@ */ package org.springframework.data.mongodb.core.convert.encryption; +import static java.util.Arrays.*; +import static java.util.Collections.*; +import static org.springframework.data.mongodb.core.encryption.EncryptionOptions.*; + import java.util.Collection; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; @@ -27,28 +32,36 @@ import org.bson.BsonValue; import org.bson.Document; import org.bson.types.Binary; + +import org.jspecify.annotations.Nullable; import org.springframework.core.CollectionFactory; import org.springframework.data.mongodb.core.convert.MongoConversionContext; +import org.springframework.data.mongodb.core.convert.MongoConversionContext.OperatorContext; import org.springframework.data.mongodb.core.encryption.Encryption; import org.springframework.data.mongodb.core.encryption.EncryptionContext; +import org.springframework.data.mongodb.core.encryption.EncryptionKey; import org.springframework.data.mongodb.core.encryption.EncryptionKeyResolver; import org.springframework.data.mongodb.core.encryption.EncryptionOptions; import org.springframework.data.mongodb.core.mapping.Encrypted; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.core.mapping.Queryable; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; /** * Default implementation of {@link EncryptingConverter}. Properties used with this converter must be annotated with * {@link Encrypted @Encrypted} to provide key and algorithm metadata. * * @author Christoph Strobl + * @author Ross Lawley * @since 4.1 */ public class MongoEncryptionConverter implements EncryptingConverter { private static final Log LOGGER = LogFactory.getLog(MongoEncryptionConverter.class); + private static final List RANGE_OPERATORS = asList("$gt", "$gte", "$lt", "$lte"); + public static final String AND_OPERATOR = "$and"; private final Encryption encryption; private final EncryptionKeyResolver keyResolver; @@ -59,16 +72,15 @@ public MongoEncryptionConverter(Encryption encryption, En this.keyResolver = keyResolver; } - @Nullable @Override - public Object read(Object value, MongoConversionContext context) { + public @Nullable Object read(Object value, MongoConversionContext context) { Object decrypted = EncryptingConverter.super.read(value, context); return decrypted instanceof BsonValue bsonValue ? BsonUtils.toJavaType(bsonValue) : decrypted; } @Override - public Object decrypt(Object encryptedValue, EncryptionContext context) { + public @Nullable Object decrypt(Object encryptedValue, EncryptionContext context) { Object decryptedValue = encryptedValue; if (encryptedValue instanceof Binary || encryptedValue instanceof BsonBinary) { @@ -142,7 +154,8 @@ public Object decrypt(Object encryptedValue, EncryptionContext context) { } @Override - public Object encrypt(Object value, EncryptionContext context) { + @SuppressWarnings("NullAway") + public Object encrypt(@Nullable Object value, EncryptionContext context) { if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("Encrypting %s.%s.", getProperty(context).getOwner().getName(), @@ -158,10 +171,52 @@ public Object encrypt(Object value, EncryptionContext context) { if (annotation == null) { throw new IllegalStateException(String.format("Property %s.%s is not annotated with @Encrypted", - getProperty(context).getOwner().getName(), getProperty(context).getName())); + persistentProperty.getOwner().getName(), persistentProperty.getName())); + } + + String algorithm = annotation.algorithm(); + EncryptionKey key = keyResolver.getKey(context); + OperatorContext operatorContext = context.getOperatorContext(); + + EncryptionOptions encryptionOptions = new EncryptionOptions(algorithm, key, + getEQOptions(persistentProperty, operatorContext)); + + if (operatorContext != null && !operatorContext.isWriteOperation() && encryptionOptions.queryableEncryptionOptions() != null + && !encryptionOptions.queryableEncryptionOptions().getQueryType().equals("equality")) { + return encryptExpression(operatorContext, value, encryptionOptions); + } else { + return encryptValue(value, context, persistentProperty, encryptionOptions); + } + } + + private static @Nullable QueryableEncryptionOptions getEQOptions(MongoPersistentProperty persistentProperty, + @Nullable OperatorContext operatorContext) { + + Queryable queryableAnnotation = persistentProperty.findAnnotation(Queryable.class); + if (queryableAnnotation == null || !StringUtils.hasText(queryableAnnotation.queryType())) { + return null; + } + + QueryableEncryptionOptions queryableEncryptionOptions = QueryableEncryptionOptions.none(); + + String queryAttributes = queryableAnnotation.queryAttributes(); + if (!queryAttributes.isEmpty()) { + queryableEncryptionOptions = queryableEncryptionOptions.attributes(Document.parse(queryAttributes)); + } + + if (queryableAnnotation.contentionFactor() >= 0) { + queryableEncryptionOptions = queryableEncryptionOptions.contentionFactor(queryableAnnotation.contentionFactor()); } - EncryptionOptions encryptionOptions = new EncryptionOptions(annotation.algorithm(), keyResolver.getKey(context)); + boolean isPartOfARangeQuery = operatorContext != null && !operatorContext.isWriteOperation(); + if (isPartOfARangeQuery) { + queryableEncryptionOptions = queryableEncryptionOptions.queryType(queryableAnnotation.queryType()); + } + return queryableEncryptionOptions; + } + + private BsonBinary encryptValue(Object value, EncryptionContext context, MongoPersistentProperty persistentProperty, + EncryptionOptions encryptionOptions) { if (!persistentProperty.isEntity()) { @@ -176,6 +231,7 @@ public Object encrypt(Object value, EncryptionContext context) { } return encryption.encrypt(BsonUtils.simpleToBsonValue(value), encryptionOptions); } + if (persistentProperty.isCollectionLike()) { return encryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptionOptions); } @@ -187,6 +243,37 @@ public Object encrypt(Object value, EncryptionContext context) { return encryption.encrypt(BsonUtils.simpleToBsonValue(write), encryptionOptions); } + /** + * Encrypts a range query expression. + *

+ * The mongodb-crypt {@code encryptExpression} has strict formatting requirements so this method ensures these + * requirements are met and then picks out and returns just the value for use with a range query. + * + * @param operatorContext field name and query operator. + * @param value the value of the expression to be encrypted. + * @param encryptionOptions the options. + * @return the encrypted range value for use in a range query. + */ + private BsonValue encryptExpression(OperatorContext operatorContext, Object value, + EncryptionOptions encryptionOptions) { + + BsonValue doc = BsonUtils.simpleToBsonValue(value); + + String fieldName = operatorContext.path(); + String queryOperator = operatorContext.operator(); + + if (!RANGE_OPERATORS.contains(queryOperator)) { + throw new AssertionError(String.format("Not a valid range query. Querying a range encrypted field but the " + + "query operator '%s' for field path '%s' is not a range query.", queryOperator, fieldName)); + } + + BsonDocument encryptExpression = new BsonDocument(AND_OPERATOR, + new BsonArray(singletonList(new BsonDocument(fieldName, new BsonDocument(queryOperator, doc))))); + + BsonDocument result = encryption.encryptExpression(encryptExpression, encryptionOptions); + return result.getArray(AND_OPERATOR).get(0).asDocument().getDocument(fieldName).getBinary(queryOperator); + } + private BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property, EncryptionContext context) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/package-info.java index 4a6f78357a..a0e8ea27f7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/package-info.java @@ -3,5 +3,5 @@ * explicit encryption * mechanism of Client-Side Field Level Encryption. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.convert.encryption; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/package-info.java index cfa07fa8f9..dbef5cbb90 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/package-info.java @@ -1,6 +1,6 @@ /** * Spring Data MongoDB specific converter infrastructure. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.convert; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java index 5645c1e416..a80a72ed1f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java @@ -15,13 +15,18 @@ */ package org.springframework.data.mongodb.core.encryption; +import org.bson.BsonDocument; + /** * Component responsible for encrypting and decrypting values. * + * @param

plaintext type. + * @param ciphertext type. * @author Christoph Strobl + * @author Ross Lawley * @since 4.1 */ -public interface Encryption { +public interface Encryption { /** * Encrypt the given value. @@ -30,7 +35,7 @@ public interface Encryption { * @param options must not be {@literal null}. * @return the encrypted value. */ - T encrypt(S value, EncryptionOptions options); + C encrypt(P value, EncryptionOptions options); /** * Decrypt the given value. @@ -38,6 +43,18 @@ public interface Encryption { * @param value must not be {@literal null}. * @return the decrypted value. */ - S decrypt(T value); + P decrypt(C value); + + /** + * Encrypt the given expression. + * + * @param value must not be {@literal null}. + * @param options must not be {@literal null}. + * @return the encrypted expression. + * @since 4.5.0 + */ + default BsonDocument encryptExpression(BsonDocument value, EncryptionOptions options) { + throw new UnsupportedOperationException("Unsupported encryption method"); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java index 89beaadedb..45e83ed7ca 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java @@ -15,16 +15,18 @@ */ package org.springframework.data.mongodb.core.encryption; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mongodb.core.convert.MongoConversionContext.OperatorContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; import org.springframework.expression.EvaluationContext; -import org.springframework.lang.Nullable; /** * Context to encapsulate encryption for a specific {@link MongoPersistentProperty}. * * @author Christoph Strobl + * @author Ross Lawley * @since 4.1 */ public interface EncryptionContext { @@ -43,7 +45,7 @@ public interface EncryptionContext { * @param value * @return */ - Object convertToMongoType(Object value); + @Nullable Object convertToMongoType(Object value); /** * Reads the value as an instance of the {@link PersistentProperty#getTypeInformation() property type}. @@ -52,7 +54,7 @@ public interface EncryptionContext { * @return can be {@literal null}. * @throws IllegalStateException if value cannot be read as an instance of {@link Class type}. */ - default T read(@Nullable Object value) { + default @Nullable T read(@Nullable Object value) { return (T) read(value, getProperty().getTypeInformation()); } @@ -64,7 +66,7 @@ default T read(@Nullable Object value) { * @return can be {@literal null}. * @throws IllegalStateException if value cannot be read as an instance of {@link Class type}. */ - default T read(@Nullable Object value, Class target) { + default @Nullable T read(@Nullable Object value, Class target) { return read(value, TypeInformation.of(target)); } @@ -76,7 +78,7 @@ default T read(@Nullable Object value, Class target) { * @return can be {@literal null}. * @throws IllegalStateException if value cannot be read as an instance of {@link Class type}. */ - T read(@Nullable Object value, TypeInformation target); + @Nullable T read(@Nullable Object value, TypeInformation target); /** * Write the value as an instance of the {@link PersistentProperty#getTypeInformation() property type}. @@ -88,8 +90,7 @@ default T read(@Nullable Object value, Class target) { * @see PersistentProperty#getTypeInformation() * @see #write(Object, TypeInformation) */ - @Nullable - default T write(@Nullable Object value) { + default @Nullable T write(@Nullable Object value) { return (T) write(value, getProperty().getTypeInformation()); } @@ -101,8 +102,7 @@ default T write(@Nullable Object value) { * @return can be {@literal null}. * @throws IllegalStateException if value cannot be written as an instance of {@link Class type}. */ - @Nullable - default T write(@Nullable Object value, Class target) { + default @Nullable T write(@Nullable Object value, Class target) { return write(value, TypeInformation.of(target)); } @@ -114,8 +114,7 @@ default T write(@Nullable Object value, Class target) { * @return can be {@literal null}. * @throws IllegalStateException if value cannot be written as an instance of {@link Class type}. */ - @Nullable - T write(@Nullable Object value, TypeInformation target); + @Nullable T write(@Nullable Object value, TypeInformation target); /** * Lookup the value for a given path within the current context. @@ -126,6 +125,15 @@ default T write(@Nullable Object value, Class target) { @Nullable Object lookupValue(String path); - EvaluationContext getEvaluationContext(Object source); + EvaluationContext getEvaluationContext(@Nullable Object source); + /** + * The field name and field query operator + * + * @return can be {@literal null}. + */ + @Nullable + default OperatorContext getOperatorContext() { + return null; + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java index fe01cfa8ba..7a3e8a2c76 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java @@ -15,27 +15,41 @@ */ package org.springframework.data.mongodb.core.encryption; +import java.util.Map; +import java.util.Objects; + +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** - * Options, like the {@link #algorithm()}, to apply when encrypting values. - * + * Options used to provide additional information when {@link Encryption encrypting} values. like the + * {@link #algorithm()} to be used. + * * @author Christoph Strobl + * @author Ross Lawley * @since 4.1 */ public class EncryptionOptions { private final String algorithm; private final EncryptionKey key; + private final @Nullable QueryableEncryptionOptions queryableEncryptionOptions; public EncryptionOptions(String algorithm, EncryptionKey key) { + this(algorithm, key, null); + } + + public EncryptionOptions(String algorithm, EncryptionKey key, + @Nullable QueryableEncryptionOptions queryableEncryptionOptions) { Assert.hasText(algorithm, "Algorithm must not be empty"); Assert.notNull(key, "EncryptionKey must not be empty"); + Assert.notNull(key, "QueryableEncryptionOptions must not be empty"); this.key = key; this.algorithm = algorithm; + this.queryableEncryptionOptions = queryableEncryptionOptions; } public EncryptionKey key() { @@ -46,6 +60,14 @@ public String algorithm() { return algorithm; } + /** + * @return {@literal null} if not set. + * @since 4.5 + */ + public @Nullable QueryableEncryptionOptions queryableEncryptionOptions() { + return queryableEncryptionOptions; + } + @Override public boolean equals(Object o) { @@ -61,7 +83,11 @@ public boolean equals(Object o) { if (!ObjectUtils.nullSafeEquals(algorithm, that.algorithm)) { return false; } - return ObjectUtils.nullSafeEquals(key, that.key); + if (!ObjectUtils.nullSafeEquals(key, that.key)) { + return false; + } + + return ObjectUtils.nullSafeEquals(queryableEncryptionOptions, that.queryableEncryptionOptions); } @Override @@ -69,11 +95,141 @@ public int hashCode() { int result = ObjectUtils.nullSafeHashCode(algorithm); result = 31 * result + ObjectUtils.nullSafeHashCode(key); + result = 31 * result + ObjectUtils.nullSafeHashCode(queryableEncryptionOptions); return result; } @Override public String toString() { - return "EncryptionOptions{" + "algorithm='" + algorithm + '\'' + ", key=" + key + '}'; + return "EncryptionOptions{" + "algorithm='" + algorithm + '\'' + ", key=" + key + ", queryableEncryptionOptions='" + + queryableEncryptionOptions + "'}"; + } + + /** + * Options, like the {@link #getQueryType()}, to apply when encrypting queryable values. + * + * @author Ross Lawley + * @author Christoph Strobl + * @since 4.5 + */ + public static class QueryableEncryptionOptions { + + private static final QueryableEncryptionOptions NONE = new QueryableEncryptionOptions(null, null, Map.of()); + + private final @Nullable String queryType; + private final @Nullable Long contentionFactor; + private final Map attributes; + + private QueryableEncryptionOptions(@Nullable String queryType, @Nullable Long contentionFactor, + Map attributes) { + + this.queryType = queryType; + this.contentionFactor = contentionFactor; + this.attributes = attributes; + } + + /** + * Create an empty {@link QueryableEncryptionOptions}. + * + * @return unmodifiable {@link QueryableEncryptionOptions} instance. + */ + public static QueryableEncryptionOptions none() { + return NONE; + } + + /** + * Define the {@code queryType} to be used for queryable document encryption. + * + * @param queryType can be {@literal null}. + * @return new instance of {@link QueryableEncryptionOptions}. + */ + public QueryableEncryptionOptions queryType(@Nullable String queryType) { + return new QueryableEncryptionOptions(queryType, contentionFactor, attributes); + } + + /** + * Define the {@code contentionFactor} to be used for queryable document encryption. + * + * @param contentionFactor can be {@literal null}. + * @return new instance of {@link QueryableEncryptionOptions}. + */ + public QueryableEncryptionOptions contentionFactor(@Nullable Long contentionFactor) { + return new QueryableEncryptionOptions(queryType, contentionFactor, attributes); + } + + /** + * Define the {@code rangeOptions} to be used for queryable document encryption. + * + * @param attributes can be {@literal null}. + * @return new instance of {@link QueryableEncryptionOptions}. + */ + public QueryableEncryptionOptions attributes(Map attributes) { + return new QueryableEncryptionOptions(queryType, contentionFactor, attributes); + } + + /** + * Get the {@code queryType} to apply. + * + * @return {@literal null} if not set. + */ + public @Nullable String getQueryType() { + return queryType; + } + + /** + * Get the {@code contentionFactor} to apply. + * + * @return {@literal null} if not set. + */ + public @Nullable Long getContentionFactor() { + return contentionFactor; + } + + /** + * Get the {@code rangeOptions} to apply. + * + * @return never {@literal null}. + */ + public Map getAttributes() { + return Map.copyOf(attributes); + } + + /** + * @return {@literal true} if no arguments set. + */ + boolean isEmpty() { + return getQueryType() == null && getContentionFactor() == null && getAttributes().isEmpty(); + } + + @Override + public String toString() { + return "QueryableEncryptionOptions{" + "queryType='" + queryType + '\'' + ", contentionFactor=" + contentionFactor + + ", attributes=" + attributes + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + QueryableEncryptionOptions that = (QueryableEncryptionOptions) o; + + if (!ObjectUtils.nullSafeEquals(queryType, that.queryType)) { + return false; + } + + if (!ObjectUtils.nullSafeEquals(contentionFactor, that.contentionFactor)) { + return false; + } + return ObjectUtils.nullSafeEquals(attributes, that.attributes); + } + + @Override + public int hashCode() { + return Objects.hash(queryType, contentionFactor, attributes); + } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java index 92350ce7d7..aee5dd7f8b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java @@ -15,20 +15,26 @@ */ package org.springframework.data.mongodb.core.encryption; +import java.util.Map; import java.util.function.Supplier; import org.bson.BsonBinary; +import org.bson.BsonDocument; import org.bson.BsonValue; import org.springframework.data.mongodb.core.encryption.EncryptionKey.Type; +import org.springframework.data.mongodb.core.encryption.EncryptionOptions.QueryableEncryptionOptions; +import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.util.Assert; import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.model.vault.RangeOptions; import com.mongodb.client.vault.ClientEncryption; /** * {@link ClientEncryption} based {@link Encryption} implementation. * * @author Christoph Strobl + * @author Ross Lawley * @since 4.1 */ public class MongoClientEncryption implements Encryption { @@ -59,6 +65,19 @@ public BsonValue decrypt(BsonBinary value) { @Override public BsonBinary encrypt(BsonValue value, EncryptionOptions options) { + return getClientEncryption().encrypt(value, createEncryptOptions(options)); + } + + @Override + public BsonDocument encryptExpression(BsonDocument value, EncryptionOptions options) { + return getClientEncryption().encryptExpression(value, createEncryptOptions(options)); + } + + public ClientEncryption getClientEncryption() { + return source.get(); + } + + private EncryptOptions createEncryptOptions(EncryptionOptions options) { EncryptOptions encryptOptions = new EncryptOptions(options.algorithm()); @@ -68,11 +87,58 @@ public BsonBinary encrypt(BsonValue value, EncryptionOptions options) { encryptOptions = encryptOptions.keyId((BsonBinary) options.key().value()); } - return getClientEncryption().encrypt(value, encryptOptions); + if (options.queryableEncryptionOptions() == null) { + return encryptOptions; + } + + QueryableEncryptionOptions qeOptions = options.queryableEncryptionOptions(); + if (qeOptions.getQueryType() != null) { + encryptOptions.queryType(qeOptions.getQueryType()); + } + if (qeOptions.getContentionFactor() != null) { + encryptOptions.contentionFactor(qeOptions.getContentionFactor()); + } + if (!qeOptions.getAttributes().isEmpty()) { + encryptOptions.rangeOptions(rangeOptions(qeOptions.getAttributes())); + } + return encryptOptions; } - public ClientEncryption getClientEncryption() { - return source.get(); + protected RangeOptions rangeOptions(Map attributes) { + + RangeOptions encryptionRangeOptions = new RangeOptions(); + if (attributes.isEmpty()) { + return encryptionRangeOptions; + } + + if (attributes.containsKey("min")) { + encryptionRangeOptions.min(BsonUtils.simpleToBsonValue(attributes.get("min"))); + } + if (attributes.containsKey("max")) { + encryptionRangeOptions.max(BsonUtils.simpleToBsonValue(attributes.get("max"))); + } + if (attributes.containsKey("trimFactor")) { + Object trimFactor = attributes.get("trimFactor"); + Assert.isInstanceOf(Integer.class, trimFactor, () -> String + .format("Expected to find a %s but it turned out to be %s.", Integer.class, trimFactor.getClass())); + + encryptionRangeOptions.trimFactor((Integer) trimFactor); + } + + if (attributes.containsKey("sparsity")) { + Object sparsity = attributes.get("sparsity"); + Assert.isInstanceOf(Number.class, sparsity, + () -> String.format("Expected to find a %s but it turned out to be %s.", Long.class, sparsity.getClass())); + encryptionRangeOptions.sparsity(((Number) sparsity).longValue()); + } + + if (attributes.containsKey("precision")) { + Object precision = attributes.get("precision"); + Assert.isInstanceOf(Number.class, precision, () -> String + .format("Expected to find a %s but it turned out to be %s.", Integer.class, precision.getClass())); + encryptionRangeOptions.precision(((Number) precision).intValue()); + } + return encryptionRangeOptions; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/package-info.java index f3906d89dd..90a3ab8720 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/package-info.java @@ -2,5 +2,5 @@ * Infrastructure for explicit * encryption mechanism of Client-Side Field Level Encryption. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.encryption; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonGeometryCollection.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonGeometryCollection.java index 2372700aec..74f36e3198 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonGeometryCollection.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonGeometryCollection.java @@ -19,7 +19,7 @@ import java.util.Collections; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonModule.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonModule.java index bc74a56df3..5c458329ab 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonModule.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonModule.java @@ -20,8 +20,9 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.data.geo.Point; -import org.springframework.lang.Nullable; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.Version; @@ -139,9 +140,8 @@ private static void registerDeserializersIn(SimpleModule module) { */ private static abstract class GeoJsonDeserializer> extends JsonDeserializer { - @Nullable @Override - public T deserialize(@Nullable JsonParser jp, @Nullable DeserializationContext ctxt) throws IOException { + public @Nullable T deserialize(JsonParser jp, @Nullable DeserializationContext ctxt) throws IOException { JsonNode node = jp.readValueAsTree(); JsonNode coordinates = node.get("coordinates"); @@ -158,18 +158,16 @@ public T deserialize(@Nullable JsonParser jp, @Nullable DeserializationContext c * @param coordinates * @return */ - @Nullable - protected abstract T doDeserialize(ArrayNode coordinates); + protected abstract @Nullable T doDeserialize(ArrayNode coordinates); /** * Get the {@link GeoJsonPoint} representation of given {@link ArrayNode} assuming {@code node.[0]} represents * {@literal x - coordinate} and {@code node.[1]} is {@literal y}. * * @param node can be {@literal null}. - * @return {@literal null} when given a {@code null} value. + * @return {@literal null} when given a {@literal null} value. */ - @Nullable - protected GeoJsonPoint toGeoJsonPoint(@Nullable ArrayNode node) { + protected @Nullable GeoJsonPoint toGeoJsonPoint(@Nullable ArrayNode node) { if (node == null) { return null; @@ -183,10 +181,9 @@ protected GeoJsonPoint toGeoJsonPoint(@Nullable ArrayNode node) { * {@literal x - coordinate} and {@code node.[1]} is {@literal y}. * * @param node can be {@literal null}. - * @return {@literal null} when given a {@code null} value. + * @return {@literal null} when given a {@literal null} value. */ - @Nullable - protected Point toPoint(@Nullable ArrayNode node) { + protected @Nullable Point toPoint(@Nullable ArrayNode node) { if (node == null) { return null; @@ -199,7 +196,7 @@ protected Point toPoint(@Nullable ArrayNode node) { * Get the points nested within given {@link ArrayNode}. * * @param node can be {@literal null}. - * @return {@literal empty list} when given a {@code null} value. + * @return {@literal empty list} when given a {@literal null} value. */ protected List toPoints(@Nullable ArrayNode node) { @@ -236,9 +233,8 @@ protected GeoJsonLineString toLineString(ArrayNode node) { */ private static class GeoJsonPointDeserializer extends GeoJsonDeserializer { - @Nullable @Override - protected GeoJsonPoint doDeserialize(ArrayNode coordinates) { + protected @Nullable GeoJsonPoint doDeserialize(ArrayNode coordinates) { return toGeoJsonPoint(coordinates); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiLineString.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiLineString.java index 8dafe9ea00..833a1dd9f6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiLineString.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiLineString.java @@ -19,8 +19,8 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.geo.Point; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPoint.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPoint.java index bcb4c3e79e..e30ed5d6ed 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPoint.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPoint.java @@ -20,8 +20,8 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.geo.Point; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPolygon.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPolygon.java index 12b9de9da4..a7e6306b49 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPolygon.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPolygon.java @@ -19,7 +19,7 @@ import java.util.Collections; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonPolygon.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonPolygon.java index 166a10df08..990be290cd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonPolygon.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonPolygon.java @@ -21,9 +21,10 @@ import java.util.Iterator; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.geo.Point; import org.springframework.data.geo.Polygon; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -78,6 +79,7 @@ public GeoJsonPolygon(List points) { * @return new {@link GeoJsonPolygon}. * @since 1.10 */ + @Contract("_, _, _, _, _ -> new") public GeoJsonPolygon withInnerRing(Point first, Point second, Point third, Point fourth, Point... others) { return withInnerRing(asList(first, second, third, fourth, others)); } @@ -88,6 +90,7 @@ public GeoJsonPolygon withInnerRing(Point first, Point second, Point third, Poin * @param points must not be {@literal null}. * @return new {@link GeoJsonPolygon}. */ + @Contract("_ -> new") public GeoJsonPolygon withInnerRing(List points) { return withInnerRing(new GeoJsonLineString(points)); } @@ -99,6 +102,7 @@ public GeoJsonPolygon withInnerRing(List points) { * @return new {@link GeoJsonPolygon}. * @since 1.10 */ + @Contract("_ -> new") public GeoJsonPolygon withInnerRing(GeoJsonLineString lineString) { Assert.notNull(lineString, "LineString must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Sphere.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Sphere.java index a482c136e7..d3ca840d6b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Sphere.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Sphere.java @@ -18,12 +18,12 @@ import java.util.Arrays; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; import org.springframework.data.geo.Shape; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -63,7 +63,7 @@ public Sphere(Point center, Distance radius) { * @param radius */ public Sphere(Point center, double radius) { - this(center, new Distance(radius)); + this(center, Distance.of(radius)); } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/package-info.java index 6cc77f832b..e5adfb26f4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/package-info.java @@ -1,6 +1,6 @@ /** * Support for MongoDB geo-spatial queries. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.geo; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/DefaultSearchIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/DefaultSearchIndexOperations.java index 225bb41ac8..b4b7b8430a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/DefaultSearchIndexOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/DefaultSearchIndexOperations.java @@ -20,12 +20,12 @@ import org.bson.BsonString; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import com.mongodb.client.model.SearchIndexModel; @@ -40,7 +40,7 @@ public class DefaultSearchIndexOperations implements SearchIndexOperations { private final MongoOperations mongoOperations; private final String collectionName; - private final TypeInformation entityTypeInformation; + private final @Nullable TypeInformation entityTypeInformation; public DefaultSearchIndexOperations(MongoOperations mongoOperations, Class type) { this(mongoOperations, mongoOperations.getCollectionName(type), type); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java index 3fb797559b..a39da5c946 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java @@ -114,16 +114,6 @@ */ GeoSpatialIndexType type() default GeoSpatialIndexType.GEO_2D; - /** - * The bucket size for {@link GeoSpatialIndexType#GEO_HAYSTACK} indexes, in coordinate units. - * - * @since 1.4 - * @return {@literal 1.0} by default. - * @deprecated since MongoDB server version 4.4 - */ - @Deprecated - double bucketSize() default 1.0; - /** * The name of the additional field to use for {@link GeoSpatialIndexType#GEO_HAYSTACK} indexes * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java index 0949506195..c1ce25776b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java @@ -18,9 +18,9 @@ import java.util.Optional; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Collation; -import org.springframework.data.mongodb.util.MongoClientVersion; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -41,7 +41,6 @@ public class GeospatialIndex implements IndexDefinition { private @Nullable Integer max; private @Nullable Integer bits; private GeoSpatialIndexType type = GeoSpatialIndexType.GEO_2D; - private Double bucketSize = MongoClientVersion.isVersion5orNewer() ? null : 1.0; private @Nullable String additionalField; private Optional filter = Optional.empty(); private Optional collation = Optional.empty(); @@ -62,6 +61,7 @@ public GeospatialIndex(String field) { * @param name must not be {@literal null} or empty. * @return this. */ + @Contract("_ -> this") public GeospatialIndex named(String name) { this.name = name; @@ -72,6 +72,7 @@ public GeospatialIndex named(String name) { * @param min * @return this. */ + @Contract("_ -> this") public GeospatialIndex withMin(int min) { this.min = min; return this; @@ -81,6 +82,7 @@ public GeospatialIndex withMin(int min) { * @param max * @return this. */ + @Contract("_ -> this") public GeospatialIndex withMax(int max) { this.max = max; return this; @@ -90,6 +92,7 @@ public GeospatialIndex withMax(int max) { * @param bits * @return this. */ + @Contract("_ -> this") public GeospatialIndex withBits(int bits) { this.bits = bits; return this; @@ -99,6 +102,7 @@ public GeospatialIndex withBits(int bits) { * @param type must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public GeospatialIndex typed(GeoSpatialIndexType type) { Assert.notNull(type, "Type must not be null"); @@ -107,21 +111,11 @@ public GeospatialIndex typed(GeoSpatialIndexType type) { return this; } - /** - * @param bucketSize - * @return this. - * @deprecated since MongoDB server version 4.4 - */ - @Deprecated - public GeospatialIndex withBucketSize(double bucketSize) { - this.bucketSize = bucketSize; - return this; - } - /** * @param fieldName * @return this. */ + @Contract("_ -> this") public GeospatialIndex withAdditionalField(String fieldName) { this.additionalField = fieldName; return this; @@ -136,6 +130,7 @@ public GeospatialIndex withAdditionalField(String fieldName) { * "https://docs.mongodb.com/manual/core/index-partial/">https://docs.mongodb.com/manual/core/index-partial/ * @since 1.10 */ + @Contract("_ -> this") public GeospatialIndex partial(@Nullable IndexFilter filter) { this.filter = Optional.ofNullable(filter); @@ -152,6 +147,7 @@ public GeospatialIndex partial(@Nullable IndexFilter filter) { * @return this. * @since 2.0 */ + @Contract("_ -> this") public GeospatialIndex collation(@Nullable Collation collation) { this.collation = Optional.ofNullable(collation); @@ -203,14 +199,9 @@ public Document getIndexOptions() { break; case GEO_2DSPHERE: - break; case GEO_HAYSTACK: - - if (bucketSize != null) { - document.put("bucketSize", bucketSize); - } break; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java index 95f4226e28..91195a40f4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java @@ -23,10 +23,11 @@ import java.util.concurrent.TimeUnit; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.index.IndexOptions.Unique; import org.springframework.data.mongodb.core.query.Collation; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -52,11 +53,13 @@ public Index(String key, Direction direction) { fieldSpec.put(key, direction); } + @Contract("_, _ -> this") public Index on(String key, Direction direction) { fieldSpec.put(key, direction); return this; } + @Contract("_ -> this") public Index named(String name) { this.name = name; return this; @@ -69,6 +72,7 @@ public Index named(String name) { * @see https://docs.mongodb.org/manual/core/index-unique/ */ + @Contract("-> this") public Index unique() { this.options.setUnique(Unique.YES); @@ -82,6 +86,7 @@ public Index unique() { * @see https://docs.mongodb.org/manual/core/index-sparse/ */ + @Contract("-> this") public Index sparse() { this.sparse = true; return this; @@ -92,7 +97,7 @@ public Index sparse() { * * @return this. * @since 1.5 - */ + */@Contract("-> this") public Index background() { this.background = true; @@ -107,6 +112,7 @@ public Index background() { * "https://www.mongodb.com/docs/manual/core/index-hidden/">https://www.mongodb.com/docs/manual/core/index-hidden/ * @since 4.1 */ + @Contract("-> this") public Index hidden() { options.setHidden(true); @@ -120,6 +126,7 @@ public Index hidden() { * @return this. * @since 1.5 */ + @Contract("_ -> this") public Index expire(long value) { return expire(value, TimeUnit.SECONDS); } @@ -132,6 +139,7 @@ public Index expire(long value) { * @throws IllegalArgumentException if given {@literal timeout} is {@literal null}. * @since 2.2 */ + @Contract("_ -> this") public Index expire(Duration timeout) { Assert.notNull(timeout, "Timeout must not be null"); @@ -146,6 +154,7 @@ public Index expire(Duration timeout) { * @return this. * @since 1.5 */ + @Contract("_, _ -> this") public Index expire(long value, TimeUnit unit) { Assert.notNull(unit, "TimeUnit for expiration must not be null"); @@ -162,6 +171,7 @@ public Index expire(long value, TimeUnit unit) { * "https://docs.mongodb.com/manual/core/index-partial/">https://docs.mongodb.com/manual/core/index-partial/ * @since 1.10 */ + @Contract("_ -> this") public Index partial(@Nullable IndexFilter filter) { this.filter = Optional.ofNullable(filter); @@ -178,6 +188,7 @@ public Index partial(@Nullable IndexFilter filter) { * @return this. * @since 2.0 */ + @Contract("_ -> this") public Index collation(@Nullable Collation collation) { this.collation = Optional.ofNullable(collation); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java index a5cbf6c896..2e7268699c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java @@ -15,8 +15,8 @@ */ package org.springframework.data.mongodb.core.index; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort.Direction; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -139,8 +139,7 @@ public String getKey() { * * @return the direction */ - @Nullable - public Direction getDirection() { + public @Nullable Direction getDirection() { return direction; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java index de7153bfb5..e9817746c3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java @@ -27,11 +27,12 @@ import java.util.stream.Collectors; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.NumberUtils; import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; /** * Index information for a MongoDB index. @@ -89,7 +90,7 @@ public IndexInfo(List indexFields, String name, boolean unique, bool */ public static IndexInfo indexInfoOf(Document sourceDocument) { - Document keyDbObject = (Document) sourceDocument.get("key"); + Document keyDbObject = sourceDocument.get("key", new Document()); int numberOfElements = keyDbObject.keySet().size(); List indexFields = new ArrayList(numberOfElements); @@ -105,9 +106,10 @@ public static IndexInfo indexInfoOf(Document sourceDocument) { } else if ("text".equals(value)) { Document weights = (Document) sourceDocument.get("weights"); - - for (String fieldName : weights.keySet()) { - indexFields.add(IndexField.text(fieldName, Float.valueOf(weights.get(fieldName).toString()))); + if(weights != null) { + for (String fieldName : weights.keySet()) { + indexFields.add(IndexField.text(fieldName, Float.valueOf(weights.get(fieldName).toString()))); + } } } else { @@ -129,7 +131,7 @@ public static IndexInfo indexInfoOf(Document sourceDocument) { } } - String name = sourceDocument.get("name").toString(); + String name = ObjectUtils.nullSafeToString(sourceDocument.get("name")); boolean unique = sourceDocument.get("unique", false); boolean sparse = sourceDocument.get("sparse", false); @@ -161,8 +163,7 @@ public static IndexInfo indexInfoOf(Document sourceDocument) { * @return the {@link String} representation of the partial filter {@link Document}. * @since 2.1.11 */ - @Nullable - private static String extractPartialFilterString(Document sourceDocument) { + private static @Nullable String extractPartialFilterString(Document sourceDocument) { if (!sourceDocument.containsKey("partialFilterExpression")) { return null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsProvider.java index ca3d951c94..aec1ba817d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsProvider.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsProvider.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.index; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Provider interface to obtain {@link IndexOperations} by MongoDB collection name or entity type. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOptions.java index 887542cb0c..a390d1eb3e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOptions.java @@ -18,7 +18,7 @@ import java.time.Duration; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Changeable properties of an index. Can be used for index creation and modification. @@ -28,14 +28,11 @@ */ public class IndexOptions { - @Nullable - private Duration expire; + private @Nullable Duration expire; - @Nullable - private Boolean hidden; + private @Nullable Boolean hidden; - @Nullable - private Unique unique; + private @Nullable Unique unique; public enum Unique { @@ -108,8 +105,7 @@ public void setExpire(Duration expire) { /** * @return {@literal true} if hidden, {@literal null} if not set. */ - @Nullable - public Boolean isHidden() { + public @Nullable Boolean isHidden() { return hidden; } @@ -123,8 +119,7 @@ public void setHidden(boolean hidden) { /** * @return the unique property value, {@literal null} if not set. */ - @Nullable - public Unique getUnique() { + public @Nullable Unique getUnique() { return unique; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexPredicate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexPredicate.java index 362247725f..3bb3fdbd0f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexPredicate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexPredicate.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.index; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * @author Jon Brisbin @@ -26,8 +26,7 @@ public abstract class IndexPredicate { private IndexDirection direction = IndexDirection.ASCENDING; private boolean unique = false; - @Nullable - public String getName() { + public @Nullable String getName() { return name; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java index e20b0704cc..f1550d1501 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java @@ -21,7 +21,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationListener; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.mapping.PersistentEntity; @@ -33,7 +33,6 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.util.MongoDbErrorCodes; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -183,8 +182,7 @@ public boolean isIndexCreatorFor(MappingContext context) { return this.mappingContext.equals(context); } - @Nullable - private IndexInfo fetchIndexInformation(@Nullable IndexDefinitionHolder indexDefinition) { + private @Nullable IndexInfo fetchIndexInformation(@Nullable IndexDefinitionHolder indexDefinition) { if (indexDefinition == null) { return null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java index a5988b8c1d..b7beaaa3e1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java @@ -25,7 +25,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; @@ -33,6 +32,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Sort; @@ -55,13 +55,11 @@ import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.mongodb.util.DotPath; import org.springframework.data.mongodb.util.DurationUtil; -import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.data.mongodb.util.spel.ExpressionUtils; import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.util.TypeInformation; import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -112,7 +110,7 @@ public Iterable resolveIndexFor(TypeInformation * {@link GeospatialIndex}. The given {@literal root} has therefore to be annotated with {@link Document}. * * @param root must not be null. - * @return List of {@link IndexDefinitionHolder}. Will never be {@code null}. + * @return List of {@link IndexDefinitionHolder}. Will never be {@literal null}. * @throws IllegalArgumentException in case of missing {@link Document} annotation marking root entities. */ public List resolveIndexForEntity(MongoPersistentEntity root) { @@ -165,7 +163,7 @@ private void potentiallyAddIndexForProperty(MongoPersistentEntity root, Mongo } if (persistentProperty.isEntity()) { - indexes.addAll(resolveIndexForEntity(mappingContext.getPersistentEntity(persistentProperty), + indexes.addAll(resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(persistentProperty), persistentProperty.isUnwrapped() ? "" : persistentProperty.getFieldName(), Path.of(persistentProperty), root.getCollection(), guard)); } @@ -191,7 +189,7 @@ private void potentiallyAddIndexForProperty(MongoPersistentEntity root, Mongo * @param collection * @param guard * @return List of {@link IndexDefinitionHolder} representing indexes for given type and its referenced property - * types. Will never be {@code null}. + * types. Will never be {@literal null}. */ private List resolveIndexForClass(TypeInformation type, String dotPath, Path path, String collection, CycleGuard guard) { @@ -232,7 +230,7 @@ private void guardAndPotentiallyAddIndexForProperty(MongoPersistentProperty pers if (persistentProperty.isEntity()) { try { - indexes.addAll(resolveIndexForEntity(mappingContext.getPersistentEntity(persistentProperty), + indexes.addAll(resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(persistentProperty), propertyDotPath.toString(), propertyPath, collection, guard)); } catch (CyclicPropertyReferenceException e) { LOGGER.info(e.getMessage()); @@ -385,7 +383,7 @@ public void doWithPersistentProperty(MongoPersistentProperty persistentProperty) try { appendTextIndexInformation(propertyDotPath, propertyPath, indexDefinitionBuilder, - mappingContext.getPersistentEntity(persistentProperty.getActualType()), optionsForNestedType, guard); + mappingContext.getRequiredPersistentEntity(persistentProperty.getActualType()), optionsForNestedType, guard); } catch (CyclicPropertyReferenceException e) { LOGGER.info(e.getMessage()); } catch (InvalidDataAccessApiUsageException e) { @@ -520,8 +518,7 @@ private org.bson.Document resolveCompoundIndexKeyFromStringDefinition(String dot * @param persistentProperty * @return */ - @Nullable - protected IndexDefinitionHolder createIndexDefinition(String dotPath, String collection, + protected @Nullable IndexDefinitionHolder createIndexDefinition(String dotPath, String collection, MongoPersistentProperty persistentProperty) { Indexed index = persistentProperty.findAnnotation(Indexed.class); @@ -577,7 +574,7 @@ protected IndexDefinitionHolder createIndexDefinition(String dotPath, String col return new IndexDefinitionHolder(dotPath, indexDefinition, collection); } - private PartialIndexFilter evaluatePartialFilter(String filterExpression, PersistentEntity entity) { + private PartialIndexFilter evaluatePartialFilter(String filterExpression, @Nullable PersistentEntity entity) { Object result = ExpressionUtils.evaluate(filterExpression, () -> getEvaluationContextForProperty(entity)); @@ -588,7 +585,7 @@ private PartialIndexFilter evaluatePartialFilter(String filterExpression, Persis return PartialIndexFilter.of(BsonUtils.parse(filterExpression, null)); } - private org.bson.Document evaluateWildcardProjection(String projectionExpression, PersistentEntity entity) { + private org.bson.Document evaluateWildcardProjection(String projectionExpression, @Nullable PersistentEntity entity) { Object result = ExpressionUtils.evaluate(projectionExpression, () -> getEvaluationContextForProperty(entity)); @@ -599,7 +596,7 @@ private org.bson.Document evaluateWildcardProjection(String projectionExpression return BsonUtils.parse(projectionExpression, null); } - private Collation evaluateCollation(String collationExpression, PersistentEntity entity) { + private Collation evaluateCollation(String collationExpression, @Nullable PersistentEntity entity) { Object result = ExpressionUtils.evaluate(collationExpression, () -> getEvaluationContextForProperty(entity)); if (result instanceof org.bson.Document document) { @@ -692,8 +689,7 @@ public void setEvaluationContextProvider(EvaluationContextProvider evaluationCon * @param persistentProperty * @return */ - @Nullable - protected IndexDefinitionHolder createGeoSpatialIndexDefinition(String dotPath, String collection, + protected @Nullable IndexDefinitionHolder createGeoSpatialIndexDefinition(String dotPath, String collection, MongoPersistentProperty persistentProperty) { GeoSpatialIndexed index = persistentProperty.findAnnotation(GeoSpatialIndexed.class); @@ -711,23 +707,6 @@ protected IndexDefinitionHolder createGeoSpatialIndexDefinition(String dotPath, .named(pathAwareIndexName(index.name(), dotPath, persistentProperty.getOwner(), persistentProperty)); } - if (MongoClientVersion.isVersion5orNewer()) { - - Optional defaultBucketSize = MergedAnnotation.of(GeoSpatialIndexed.class).getDefaultValue("bucketSize", - Double.class); - if (!defaultBucketSize.isPresent() || index.bucketSize() != defaultBucketSize.get()) { - indexDefinition.withBucketSize(index.bucketSize()); - } else { - if (LOGGER.isInfoEnabled()) { - LOGGER.info( - "GeoSpatialIndexed.bucketSize no longer supported by Mongo Client 5 or newer. Ignoring bucketSize for path %s." - .formatted(dotPath)); - } - } - } else { - indexDefinition.withBucketSize(index.bucketSize()); - } - indexDefinition.typed(index.type()).withAdditionalField(index.additionalField()); return new IndexDefinitionHolder(dotPath, indexDefinition, collection); @@ -812,8 +791,7 @@ private static Duration computeIndexTimeout(String timeoutValue, Supplier entity) { + private @Nullable Collation resolveCollation(Annotation annotation, @Nullable PersistentEntity entity) { return MergedAnnotation.from(annotation).getValue("collation", String.class).filter(StringUtils::hasText) .map(it -> evaluateCollation(it, entity)).orElseGet(() -> { @@ -1138,8 +1116,7 @@ public IncludeStrategy getStrategy() { return strategy; } - @Nullable - public TextIndexedFieldSpec getParentFieldSpec() { + public @Nullable TextIndexedFieldSpec getParentFieldSpec() { return parentFieldSpec; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexDefinition.java index 9d4315beae..e3ea12baa9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexDefinition.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexDefinition.java @@ -16,11 +16,11 @@ package org.springframework.data.mongodb.core.index; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; /** * Definition for an Atlas Search Index (Search Index or Vector Index). diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexInfo.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexInfo.java index 1a657ecf0b..6da94dd130 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexInfo.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexInfo.java @@ -18,12 +18,12 @@ import java.util.function.Supplier; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; /** * Index information for a MongoDB Search Index. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexDefinition.java index a87b15de45..0b473388fb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexDefinition.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexDefinition.java @@ -20,9 +20,10 @@ import java.util.Set; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mongodb.core.query.Collation; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -235,6 +236,7 @@ public TextIndexDefinitionBuilder() { * @param name * @return */ + @Contract("_ -> this") public TextIndexDefinitionBuilder named(String name) { this.instance.name = name; return this; @@ -246,6 +248,7 @@ public TextIndexDefinitionBuilder named(String name) { * * @return */ + @Contract("-> this") public TextIndexDefinitionBuilder onAllFields() { if (!instance.fieldSpecs.isEmpty()) { @@ -262,6 +265,7 @@ public TextIndexDefinitionBuilder onAllFields() { * @param fieldnames * @return */ + @Contract("_ -> this") public TextIndexDefinitionBuilder onFields(String... fieldnames) { for (String fieldname : fieldnames) { @@ -276,6 +280,7 @@ public TextIndexDefinitionBuilder onFields(String... fieldnames) { * @param fieldname * @return */ + @Contract("_ -> this") public TextIndexDefinitionBuilder onField(String fieldname) { return onField(fieldname, 1F); } @@ -286,6 +291,7 @@ public TextIndexDefinitionBuilder onField(String fieldname) { * @param fieldname * @return */ + @Contract("_, _ -> this") public TextIndexDefinitionBuilder onField(String fieldname, Float weight) { if (this.instance.fieldSpecs.contains(ALL_FIELDS)) { @@ -305,6 +311,7 @@ public TextIndexDefinitionBuilder onField(String fieldname, Float weight) { * @see https://docs.mongodb.org/manual/tutorial/specify-language-for-text-index/#specify-default-language-text-index */ + @Contract("_ -> this") public TextIndexDefinitionBuilder withDefaultLanguage(String language) { this.instance.defaultLanguage = language; @@ -317,6 +324,7 @@ public TextIndexDefinitionBuilder withDefaultLanguage(String language) { * @param fieldname * @return */ + @Contract("_ -> this") public TextIndexDefinitionBuilder withLanguageOverride(String fieldname) { if (StringUtils.hasText(this.instance.languageOverride)) { @@ -338,6 +346,7 @@ public TextIndexDefinitionBuilder withLanguageOverride(String fieldname) { * "https://docs.mongodb.com/manual/core/index-partial/">https://docs.mongodb.com/manual/core/index-partial/ * @since 1.10 */ + @Contract("_ -> this") public TextIndexDefinitionBuilder partial(@Nullable IndexFilter filter) { this.instance.filter = filter; @@ -349,12 +358,14 @@ public TextIndexDefinitionBuilder partial(@Nullable IndexFilter filter) { * * @since 2.2 */ + @Contract("-> this") public TextIndexDefinitionBuilder withSimpleCollation() { this.instance.collation = Collation.simple(); return this; } + @Contract("-> new") public TextIndexDefinition build() { return this.instance; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/VectorIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/VectorIndex.java index b46dbf4d0c..aa8daa8c39 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/VectorIndex.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/VectorIndex.java @@ -20,14 +20,15 @@ import java.util.function.Consumer; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -149,7 +150,8 @@ static VectorIndex of(Document document) { for (Object entry : definition.get("fields", List.class)) { if (entry instanceof Document field) { - if (field.get("type").equals("vector")) { + Object fieldType = field.get("type"); + if (ObjectUtils.nullSafeEquals(fieldType, "vector")) { index.addField(new VectorIndexField(field.getString("path"), "vector", field.getInteger("numDimensions"), field.getString("similarity"), field.getString("quantization"))); } else { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java index dcd2b7c022..ff0a92ada1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java @@ -21,8 +21,9 @@ import java.util.concurrent.TimeUnit; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.FieldName; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -76,6 +77,7 @@ public WildcardIndex(@Nullable String path) { * * @return this. */ + @Contract("-> this") public WildcardIndex includeId() { wildcardProjection.put(FieldName.ID.name(), 1); @@ -89,6 +91,7 @@ public WildcardIndex includeId() { * @return this. */ @Override + @Contract("_ -> this") public WildcardIndex named(String name) { super.named(name); @@ -101,6 +104,7 @@ public WildcardIndex named(String name) { * @throws UnsupportedOperationException not supported for wildcard indexes. */ @Override + @Contract("-> fail") public Index unique() { throw new UnsupportedOperationException("Wildcard Index does not support 'unique'"); } @@ -111,6 +115,7 @@ public Index unique() { * @throws UnsupportedOperationException not supported for wildcard indexes. */ @Override + @Contract("-> fail") public Index expire(long seconds) { throw new UnsupportedOperationException("Wildcard Index does not support 'ttl'"); } @@ -121,6 +126,7 @@ public Index expire(long seconds) { * @throws UnsupportedOperationException not supported for wildcard indexes. */ @Override + @Contract("_, _ -> fail") public Index expire(long value, TimeUnit timeUnit) { throw new UnsupportedOperationException("Wildcard Index does not support 'ttl'"); } @@ -131,6 +137,7 @@ public Index expire(long value, TimeUnit timeUnit) { * @throws UnsupportedOperationException not supported for wildcard indexes. */ @Override + @Contract("_ -> fail") public Index expire(Duration duration) { throw new UnsupportedOperationException("Wildcard Index does not support 'ttl'"); } @@ -142,6 +149,7 @@ public Index expire(Duration duration) { * @param paths must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public WildcardIndex wildcardProjectionInclude(String... paths) { for (String path : paths) { @@ -157,6 +165,7 @@ public WildcardIndex wildcardProjectionInclude(String... paths) { * @param paths must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public WildcardIndex wildcardProjectionExclude(String... paths) { for (String path : paths) { @@ -172,6 +181,7 @@ public WildcardIndex wildcardProjectionExclude(String... paths) { * @param includeExclude must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public WildcardIndex wildcardProjection(Map includeExclude) { wildcardProjection.putAll(includeExclude); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/package-info.java index c49f501d8d..8524ee62f7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/package-info.java @@ -1,6 +1,6 @@ /** * Support for MongoDB document indexing. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.index; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java index 3d68dbaac2..e865009319 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.data.annotation.Id; import org.springframework.data.expression.ValueEvaluationContext; import org.springframework.data.expression.ValueExpression; @@ -35,6 +36,7 @@ import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.model.BasicPersistentEntity; import org.springframework.data.mongodb.MongoCollectionUtils; +import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.util.encryption.EncryptionUtils; import org.springframework.data.spel.ExpressionDependencies; import org.springframework.data.util.Lazy; @@ -42,7 +44,6 @@ import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -139,9 +140,8 @@ public String getLanguage() { return this.language; } - @Nullable @Override - public MongoPersistentProperty getTextScoreProperty() { + public @Nullable MongoPersistentProperty getTextScoreProperty() { return getPersistentProperty(TextScore.class); } @@ -151,7 +151,7 @@ public boolean hasTextScoreProperty() { } @Override - public org.springframework.data.mongodb.core.query.Collation getCollation() { + public @Nullable Collation getCollation() { Object collationValue = collationExpression != null ? collationExpression.evaluate(getValueEvaluationContext(null)) @@ -189,22 +189,22 @@ public void verify() { } @Override - public EvaluationContext getEvaluationContext(Object rootObject) { + public EvaluationContext getEvaluationContext(@Nullable Object rootObject) { return super.getEvaluationContext(rootObject); } @Override - public EvaluationContext getEvaluationContext(Object rootObject, ExpressionDependencies dependencies) { + public EvaluationContext getEvaluationContext(@Nullable Object rootObject, ExpressionDependencies dependencies) { return super.getEvaluationContext(rootObject, dependencies); } @Override - public ValueEvaluationContext getValueEvaluationContext(Object rootObject) { + public ValueEvaluationContext getValueEvaluationContext(@Nullable Object rootObject) { return super.getValueEvaluationContext(rootObject); } @Override - public ValueEvaluationContext getValueEvaluationContext(Object rootObject, ExpressionDependencies dependencies) { + public ValueEvaluationContext getValueEvaluationContext(@Nullable Object rootObject, ExpressionDependencies dependencies) { return super.getValueEvaluationContext(rootObject, dependencies); } @@ -243,7 +243,11 @@ public int compare(@Nullable MongoPersistentProperty o1, @Nullable MongoPersiste return -1; } - return o1.getFieldOrder() - o2.getFieldOrder(); + if(o1 != null && o2 != null) { + return o1.getFieldOrder() - o2.getFieldOrder(); + } + + return o1 != null ? o1.getFieldOrder() : -1; } } @@ -257,7 +261,7 @@ public int compare(@Nullable MongoPersistentProperty o1, @Nullable MongoPersiste * @return can be {@literal null}. */ @Override - protected MongoPersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNull(MongoPersistentProperty property) { + protected @Nullable MongoPersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNull(MongoPersistentProperty property) { Assert.notNull(property, "MongoPersistentProperty must not be null"); @@ -268,7 +272,7 @@ protected MongoPersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNul MongoPersistentProperty currentIdProperty = getIdProperty(); boolean currentIdPropertyIsSet = currentIdProperty != null; - @SuppressWarnings("null") + @SuppressWarnings("NullAway") boolean currentIdPropertyIsExplicit = currentIdPropertyIsSet && currentIdProperty.isExplicitIdProperty(); boolean newIdPropertyIsExplicit = property.isExplicitIdProperty(); @@ -277,7 +281,7 @@ protected MongoPersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNul } - @SuppressWarnings("null") + @SuppressWarnings("NullAway") Field currentIdPropertyField = currentIdProperty.getField(); if (newIdPropertyIsExplicit && currentIdPropertyIsExplicit) { @@ -308,8 +312,7 @@ protected MongoPersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNul * @param potentialExpression can be {@literal null} * @return can be {@literal null}. */ - @Nullable - private static ValueExpression detectExpression(@Nullable String potentialExpression) { + private static @Nullable ValueExpression detectExpression(@Nullable String potentialExpression) { if (!StringUtils.hasText(potentialExpression)) { return null; @@ -352,7 +355,7 @@ private void assertUniqueness(MongoPersistentProperty property) { } @Override - public Collection getEncryptionKeyIds() { + public @Nullable Collection getEncryptionKeyIds() { Encrypted encrypted = findAnnotation(Encrypted.class); if (encrypted == null) { @@ -405,6 +408,7 @@ private static void potentiallyAssertTextScoreType(MongoPersistentProperty persi } } + @SuppressWarnings("NullAway") private static void potentiallyAssertDBRefTargetType(MongoPersistentProperty persistentProperty) { if (persistentProperty.isDbReference() && persistentProperty.getDBRef().lazy()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java index 5c3b4e6532..027a570fa3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java @@ -23,7 +23,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - +import org.jspecify.annotations.Nullable; import org.springframework.core.env.StandardEnvironment; import org.springframework.data.expression.ValueEvaluationContext; import org.springframework.data.mapping.Association; @@ -39,7 +39,6 @@ import org.springframework.data.util.Lazy; import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -153,8 +152,7 @@ public boolean hasExplicitFieldName() { return StringUtils.hasText(getAnnotatedFieldName()); } - @Nullable - private String getAnnotatedFieldName() { + private @Nullable String getAnnotatedFieldName() { org.springframework.data.mongodb.core.mapping.Field annotation = findAnnotation( org.springframework.data.mongodb.core.mapping.Field.class); @@ -197,9 +195,8 @@ public DBRef getDBRef() { return findAnnotation(DBRef.class); } - @Nullable @Override - public DocumentReference getDocumentReference() { + public @Nullable DocumentReference getDocumentReference() { return findAnnotation(DocumentReference.class); } @@ -258,6 +255,7 @@ public MongoField getMongoField() { } @Override + @SuppressWarnings("NullAway") public Collection getEncryptionKeyIds() { Encrypted encrypted = findAnnotation(Encrypted.class); @@ -282,6 +280,7 @@ public Collection getEncryptionKeyIds() { return target; } + @SuppressWarnings("NullAway") protected MongoField doGetMongoField() { MongoFieldBuilder builder = MongoField.builder(); @@ -295,6 +294,7 @@ protected MongoField doGetMongoField() { return builder.build(); } + @SuppressWarnings("NullAway") private String doGetFieldName() { if (isIdProperty()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CachingMongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CachingMongoPersistentProperty.java index 105c38b288..eb8d08db64 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CachingMongoPersistentProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CachingMongoPersistentProperty.java @@ -15,11 +15,11 @@ */ package org.springframework.data.mongodb.core.mapping; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.model.FieldNamingStrategy; import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; /** * {@link MongoPersistentProperty} caching access to {@link #isIdProperty()} and {@link #getFieldName()}. @@ -121,12 +121,12 @@ public boolean isDbReference() { } @Override - public DBRef getDBRef() { + public @Nullable DBRef getDBRef() { return dbref.getNullable(); } @Override - public DocumentReference getDocumentReference() { + public @Nullable DocumentReference getDocumentReference() { return documentReference.getNullable(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java index 5f08e5c787..37d1019f62 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java @@ -47,6 +47,7 @@ * * * @author Christoph Strobl + * @author Ross Lawley * @since 4.1 * @see ValueConverter */ @@ -60,7 +61,8 @@ * Define the algorithm to use. *

* A {@literal Deterministic} algorithm ensures that a given input value always encrypts to the same output while a - * {@literal randomized} one will produce different results every time. + * {@literal randomized} one will produce different results every time. A {@literal range} algorithm allows for + * the value to be queried whilst encrypted. *

* Please make sure to use an algorithm that is in line with MongoDB's encryption rules for simple types, complex * objects and arrays as well as the query limitations that come with each of them. @@ -91,4 +93,5 @@ */ @AliasFor(annotation = ValueConverter.class, value = "value") Class value() default MongoEncryptionConverter.class; + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoField.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoField.java index 6f0e1ae4c3..881d741ee4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoField.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoField.java @@ -15,7 +15,9 @@ */ package org.springframework.data.mongodb.core.mapping; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.FieldName.Type; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -139,7 +141,7 @@ public String toString() { */ public static class MongoFieldBuilder { - private String name; + private @Nullable String name; private Type nameType = Type.PATH; private FieldType type = FieldType.IMPLICIT; private int order = Integer.MAX_VALUE; @@ -150,6 +152,7 @@ public static class MongoFieldBuilder { * @param fieldType * @return */ + @Contract("_ -> this") public MongoFieldBuilder fieldType(FieldType fieldType) { this.type = fieldType; @@ -163,6 +166,7 @@ public MongoFieldBuilder fieldType(FieldType fieldType) { * @param fieldName * @return */ + @Contract("_ -> this") public MongoFieldBuilder name(String fieldName) { Assert.hasText(fieldName, "Field name must not be empty"); @@ -178,6 +182,7 @@ public MongoFieldBuilder name(String fieldName) { * @param path * @return */ + @Contract("_ -> this") public MongoFieldBuilder path(String path) { Assert.hasText(path, "Field path (name) must not be empty"); @@ -193,6 +198,7 @@ public MongoFieldBuilder path(String path) { * @param order * @return */ + @Contract("_ -> this") public MongoFieldBuilder order(int order) { this.order = order; @@ -204,7 +210,10 @@ public MongoFieldBuilder order(int order) { * * @return a new {@link MongoField}. */ + @Contract("-> new") public MongoField build() { + + Assert.notNull(name, "Name of Field must not be null"); return new MongoField(new FieldName(name, nameType), type, order); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoMappingContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoMappingContext.java index 76c0269861..4540493124 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoMappingContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoMappingContext.java @@ -17,6 +17,7 @@ import java.util.AbstractMap; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -28,7 +29,6 @@ import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.util.NullableWrapperConverters; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; /** * Default implementation of a {@link MappingContext} for MongoDB using {@link BasicMongoPersistentEntity} and @@ -46,8 +46,7 @@ public class MongoMappingContext extends AbstractMappingContext getPersistentEntity(MongoPersistentProperty persistentProperty) { + public @Nullable MongoPersistentEntity getPersistentEntity(MongoPersistentProperty persistentProperty) { MongoPersistentEntity entity = super.getPersistentEntity(persistentProperty); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java index e02bd00c8d..f1d67e4ae8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java @@ -17,9 +17,10 @@ import java.util.Collection; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.model.MutablePersistentEntity; -import org.springframework.lang.Nullable; +import org.springframework.data.mongodb.core.query.Collation; /** * MongoDB specific {@link PersistentEntity} abstraction. @@ -68,8 +69,7 @@ public interface MongoPersistentEntity extends MutablePersistentEntity + * Should be valid extended {@link org.bson.Document#parse(String) JSON} representing the range options and including + * the following values: {@code min}, {@code max}, {@code trimFactor} and {@code sparsity}. + *

+ * Please note that values are data type sensitive and may require proper identification via eg. {@code $numberLong}. + * + * @return the {@link org.bson.Document#parse(String) JSON} representation of range options. + */ + @AliasFor(annotation = Queryable.class, value = "queryAttributes") + String rangeOptions() default ""; + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ShardKey.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ShardKey.java index 28a114a918..1976cc35f3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ShardKey.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ShardKey.java @@ -21,7 +21,7 @@ import java.util.List; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.ObjectUtils; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrapEntityContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrapEntityContext.java index b3b73397ff..9b42a0d37e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrapEntityContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrapEntityContext.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.mapping; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.ObjectUtils; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentEntity.java index fed08815b8..5c404a9d12 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentEntity.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentEntity.java @@ -24,6 +24,7 @@ import java.util.function.Consumer; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.springframework.core.env.Environment; import org.springframework.data.mapping.*; import org.springframework.data.mapping.model.PersistentPropertyAccessorFactory; @@ -31,7 +32,7 @@ import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.util.Streamable; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; /** * Unwrapped variant of {@link MongoPersistentEntity}. @@ -62,8 +63,7 @@ public String getLanguage() { } @Override - @Nullable - public MongoPersistentProperty getTextScoreProperty() { + public @Nullable MongoPersistentProperty getTextScoreProperty() { return delegate.getTextScoreProperty(); } @@ -73,8 +73,7 @@ public boolean hasTextScoreProperty() { } @Override - @Nullable - public Collation getCollation() { + public @Nullable Collation getCollation() { return delegate.getCollation(); } @@ -98,13 +97,7 @@ public String getName() { return delegate.getName(); } - @Override @Nullable - @Deprecated - public PreferredConstructor getPersistenceConstructor() { - return delegate.getPersistenceConstructor(); - } - @Override public InstanceCreatorMetadata getInstanceCreatorMetadata() { return delegate.getInstanceCreatorMetadata(); @@ -126,8 +119,7 @@ public boolean isVersionProperty(PersistentProperty property) { } @Override - @Nullable - public MongoPersistentProperty getIdProperty() { + public @Nullable MongoPersistentProperty getIdProperty() { return delegate.getIdProperty(); } @@ -137,8 +129,7 @@ public MongoPersistentProperty getRequiredIdProperty() { } @Override - @Nullable - public MongoPersistentProperty getVersionProperty() { + public @Nullable MongoPersistentProperty getVersionProperty() { return delegate.getVersionProperty(); } @@ -148,8 +139,7 @@ public MongoPersistentProperty getRequiredVersionProperty() { } @Override - @Nullable - public MongoPersistentProperty getPersistentProperty(String name) { + public @Nullable MongoPersistentProperty getPersistentProperty(String name) { return wrap(delegate.getPersistentProperty(name)); } @@ -165,8 +155,7 @@ public MongoPersistentProperty getRequiredPersistentProperty(String name) { } @Override - @Nullable - public MongoPersistentProperty getPersistentProperty(Class annotationType) { + public @Nullable MongoPersistentProperty getPersistentProperty(Class annotationType) { return wrap(delegate.getPersistentProperty(annotationType)); } @@ -232,8 +221,7 @@ public void doWithAssociations(SimpleAssociationHandler handler) { } @Override - @Nullable - public A findAnnotation(Class annotationType) { + public @Nullable A findAnnotation(Class annotationType) { return delegate.findAnnotation(annotationType); } @@ -295,7 +283,9 @@ public Spliterator spliterator() { return delegate.spliterator(); } - private MongoPersistentProperty wrap(MongoPersistentProperty source) { + @Contract("null -> null; !null -> !null") + private @Nullable MongoPersistentProperty wrap(@Nullable MongoPersistentProperty source) { + if (source == null) { return source; } @@ -338,7 +328,7 @@ public boolean isUnwrapped() { } @Override - public Collection getEncryptionKeyIds() { + public @Nullable Collection getEncryptionKeyIds() { return delegate.getEncryptionKeyIds(); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java index 1d4877478f..ac7f24a555 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java @@ -20,11 +20,11 @@ import java.lang.reflect.Method; import java.util.Collection; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -47,6 +47,7 @@ public UnwrappedMongoPersistentProperty(MongoPersistentProperty delegate, Unwrap } @Override + @SuppressWarnings("NullAway") public String getFieldName() { if (!context.getProperty().isUnwrapped()) { @@ -57,6 +58,7 @@ public String getFieldName() { } @Override + @SuppressWarnings("NullAway") public boolean hasExplicitFieldName() { return delegate.hasExplicitFieldName() || !ObjectUtils.isEmpty(context.getProperty().findAnnotation(Unwrapped.class).prefix()); @@ -108,14 +110,12 @@ public boolean isTextScoreProperty() { } @Override - @Nullable - public DBRef getDBRef() { + public @Nullable DBRef getDBRef() { return delegate.getDBRef(); } @Override - @Nullable - public DocumentReference getDocumentReference() { + public @Nullable DocumentReference getDocumentReference() { return delegate.getDocumentReference(); } @@ -145,6 +145,7 @@ public Class getType() { } @Override + @SuppressWarnings("NullAway") public MongoField getMongoField() { if (!context.getProperty().isUnwrapped()) { @@ -165,8 +166,7 @@ public Iterable> getPersistentEntityTypeInformation } @Override - @Nullable - public Method getGetter() { + public @Nullable Method getGetter() { return delegate.getGetter(); } @@ -176,8 +176,7 @@ public Method getRequiredGetter() { } @Override - @Nullable - public Method getSetter() { + public @Nullable Method getSetter() { return delegate.getSetter(); } @@ -187,8 +186,7 @@ public Method getRequiredSetter() { } @Override - @Nullable - public Method getWither() { + public @Nullable Method getWither() { return delegate.getWither(); } @@ -198,8 +196,7 @@ public Method getRequiredWither() { } @Override - @Nullable - public Field getField() { + public @Nullable Field getField() { return delegate.getField(); } @@ -209,14 +206,12 @@ public Field getRequiredField() { } @Override - @Nullable - public String getSpelExpression() { + public @Nullable String getSpelExpression() { return delegate.getSpelExpression(); } @Override - @Nullable - public Association getAssociation() { + public @Nullable Association getAssociation() { return delegate.getAssociation(); } @@ -291,8 +286,7 @@ public Collection getEncryptionKeyIds() { } @Override - @Nullable - public Class getComponentType() { + public @Nullable Class getComponentType() { return delegate.getComponentType(); } @@ -302,8 +296,7 @@ public Class getRawType() { } @Override - @Nullable - public Class getMapValueType() { + public @Nullable Class getMapValueType() { return delegate.getMapValueType(); } @@ -313,8 +306,7 @@ public Class getActualType() { } @Override - @Nullable - public A findAnnotation(Class annotationType) { + public @Nullable A findAnnotation(Class annotationType) { return delegate.findAnnotation(annotationType); } @@ -324,8 +316,7 @@ public A getRequiredAnnotation(Class annotationType) t } @Override - @Nullable - public A findPropertyOrOwnerAnnotation(Class annotationType) { + public @Nullable A findPropertyOrOwnerAnnotation(Class annotationType) { return delegate.findPropertyOrOwnerAnnotation(annotationType); } @@ -340,13 +331,12 @@ public boolean hasActualTypeAnnotation(Class annotationTyp } @Override - @Nullable - public Class getAssociationTargetType() { + public @Nullable Class getAssociationTargetType() { return delegate.getAssociationTargetType(); } @Override - public TypeInformation getAssociationTargetTypeInformation() { + public @Nullable TypeInformation getAssociationTargetTypeInformation() { return delegate.getAssociationTargetTypeInformation(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AbstractDeleteEvent.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AbstractDeleteEvent.java index 73f4890dec..a8e2c93773 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AbstractDeleteEvent.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AbstractDeleteEvent.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core.mapping.event; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Base class for delete events. @@ -49,8 +49,7 @@ public AbstractDeleteEvent(Document document, @Nullable Class type, String co * * @return can be {@literal null}. */ - @Nullable - public Class getType() { + public @Nullable Class getType() { return type; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterDeleteEvent.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterDeleteEvent.java index 55ccaa5f3f..10f4cdbbb7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterDeleteEvent.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterDeleteEvent.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core.mapping.event; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Event being thrown after a single or a set of documents has/have been deleted. The {@link Document} held in the event diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeDeleteEvent.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeDeleteEvent.java index 49d509fb43..c826cadb4e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeDeleteEvent.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeDeleteEvent.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core.mapping.event; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Event being thrown before a document is deleted. The {@link Document} held in the event will represent the query diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/MongoMappingEvent.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/MongoMappingEvent.java index eec9a3edf1..bec1986720 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/MongoMappingEvent.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/MongoMappingEvent.java @@ -18,8 +18,8 @@ import java.util.function.Function; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationEvent; -import org.springframework.lang.Nullable; /** * Base {@link ApplicationEvent} triggered by Spring Data MongoDB. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/package-info.java index 0cc9d071a3..71ed503b20 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/package-info.java @@ -1,6 +1,6 @@ /** * Mapping event callback infrastructure for the MongoDB document-to-object mapping subsystem. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.mapping.event; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/package-info.java index 0a513f1a18..f5c917d7d7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/package-info.java @@ -1,6 +1,6 @@ /** * Infrastructure for the MongoDB document-to-object mapping subsystem. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.mapping; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceCounts.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceCounts.java index 32a9ed5118..ed9c148a1c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceCounts.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceCounts.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.mapreduce; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Value object to encapsulate results of a map-reduce count. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceOptions.java index 9f34ec44e4..2b8c9d1eb3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceOptions.java @@ -20,10 +20,11 @@ import java.util.Optional; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Collation; -import org.springframework.lang.Nullable; import com.mongodb.client.model.MapReduceAction; +import org.springframework.lang.Contract; /** * @author Mark Pollack @@ -45,7 +46,6 @@ public class MapReduceOptions { private Boolean verbose = Boolean.TRUE; private @Nullable Integer limit; - private Optional outputSharded = Optional.empty(); private Optional finalizeFunction = Optional.empty(); private Optional collation = Optional.empty(); @@ -65,6 +65,7 @@ public static MapReduceOptions options() { * @param limit Limit the number of objects to process * @return MapReduceOptions so that methods can be chained in a fluent API style */ + @Contract("_ -> this") public MapReduceOptions limit(int limit) { this.limit = limit; @@ -78,6 +79,7 @@ public MapReduceOptions limit(int limit) { * @param collectionName The name of the collection where the results of the map-reduce operation will be stored. * @return MapReduceOptions so that methods can be chained in a fluent API style */ + @Contract("_ -> this") public MapReduceOptions outputCollection(String collectionName) { this.outputCollection = collectionName; @@ -91,6 +93,7 @@ public MapReduceOptions outputCollection(String collectionName) { * @param outputDatabase The name of the database where the results of the map-reduce operation will be stored. * @return MapReduceOptions so that methods can be chained in a fluent API style */ + @Contract("_ -> this") public MapReduceOptions outputDatabase(@Nullable String outputDatabase) { this.outputDatabase = Optional.ofNullable(outputDatabase); @@ -105,6 +108,7 @@ public MapReduceOptions outputDatabase(@Nullable String outputDatabase) { * @return this. * @since 3.0 */ + @Contract("-> this") public MapReduceOptions actionInline() { this.mapReduceAction = null; @@ -119,6 +123,7 @@ public MapReduceOptions actionInline() { * @return this. * @since 3.0 */ + @Contract("-> this") public MapReduceOptions actionMerge() { this.mapReduceAction = MapReduceAction.MERGE; @@ -133,6 +138,7 @@ public MapReduceOptions actionMerge() { * @return this. * @since 3.0 */ + @Contract("-> this") public MapReduceOptions actionReduce() { this.mapReduceAction = MapReduceAction.REDUCE; @@ -146,31 +152,20 @@ public MapReduceOptions actionReduce() { * @return MapReduceOptions so that methods can be chained in a fluent API style * @since 3.0 */ + @Contract("-> this") public MapReduceOptions actionReplace() { this.mapReduceAction = MapReduceAction.REPLACE; return this; } - /** - * If true and combined with an output mode that writes to a collection, the output collection will be sharded using - * the _id field. For MongoDB 1.9+ - * - * @param outputShared if true, output will be sharded based on _id key. - * @return MapReduceOptions so that methods can be chained in a fluent API style - */ - public MapReduceOptions outputSharded(boolean outputShared) { - - this.outputSharded = Optional.of(outputShared); - return this; - } - /** * Sets the finalize function * * @param finalizeFunction The finalize function. Can be a JSON string or a Spring Resource URL * @return MapReduceOptions so that methods can be chained in a fluent API style */ + @Contract("_ -> this") public MapReduceOptions finalizeFunction(@Nullable String finalizeFunction) { this.finalizeFunction = Optional.ofNullable(finalizeFunction); @@ -184,6 +179,7 @@ public MapReduceOptions finalizeFunction(@Nullable String finalizeFunction) { * @param scopeVariables variables that can be accessed from map, reduce, and finalize scripts * @return MapReduceOptions so that methods can be chained in a fluent API style */ + @Contract("_ -> this") public MapReduceOptions scopeVariables(Map scopeVariables) { this.scopeVariables = scopeVariables; @@ -197,6 +193,7 @@ public MapReduceOptions scopeVariables(Map scopeVariables) { * @param javaScriptMode if true, have the execution of map-reduce stay in JavaScript * @return MapReduceOptions so that methods can be chained in a fluent API style */ + @Contract("_ -> this") public MapReduceOptions javaScriptMode(boolean javaScriptMode) { this.jsMode = javaScriptMode; @@ -208,6 +205,7 @@ public MapReduceOptions javaScriptMode(boolean javaScriptMode) { * * @return MapReduceOptions so that methods can be chained in a fluent API style */ + @Contract("_ -> this") public MapReduceOptions verbose(boolean verbose) { this.verbose = verbose; @@ -221,6 +219,7 @@ public MapReduceOptions verbose(boolean verbose) { * @return * @since 2.0 */ + @Contract("_ -> this") public MapReduceOptions collation(@Nullable Collation collation) { this.collation = Optional.ofNullable(collation); @@ -231,13 +230,11 @@ public Optional getFinalizeFunction() { return this.finalizeFunction; } - @Nullable - public Boolean getJavaScriptMode() { + public @Nullable Boolean getJavaScriptMode() { return this.jsMode; } - @Nullable - public String getOutputCollection() { + public @Nullable String getOutputCollection() { return this.outputCollection; } @@ -245,10 +242,6 @@ public Optional getOutputDatabase() { return this.outputDatabase; } - public Optional getOutputSharded() { - return this.outputSharded; - } - public Map getScopeVariables() { return this.scopeVariables; } @@ -279,8 +272,7 @@ public Optional getCollation() { * @return the mapped action or {@literal null} if the action maps to inline output. * @since 2.0.10 */ - @Nullable - public MapReduceAction getMapReduceAction() { + public @Nullable MapReduceAction getMapReduceAction() { return mapReduceAction; } @@ -336,7 +328,6 @@ protected Document createOutObject() { } outputDatabase.ifPresent(val -> out.append("db", val)); - outputSharded.ifPresent(val -> out.append("sharded", val)); return out; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceResults.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceResults.java index 865a4e9438..1d4f644bd1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceResults.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceResults.java @@ -19,7 +19,7 @@ import java.util.List; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** @@ -71,13 +71,11 @@ public MapReduceCounts getCounts() { return mapReduceCounts; } - @Nullable - public String getOutputCollection() { + public @Nullable String getOutputCollection() { return outputCollection; } - @Nullable - public Document getRawResults() { + public @Nullable Document getRawResults() { return rawResults; } @@ -147,7 +145,8 @@ private static String parseOutputCollection(Document rawResults) { return null; } - return resultField instanceof Document document ? document.get("collection").toString() + return resultField instanceof Document document && document.containsKey("collection") + ? document.get("collection").toString() : resultField.toString(); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTiming.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTiming.java index 28de7fe850..d99f6d9237 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTiming.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTiming.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.mapreduce; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * @deprecated since 3.4 in favor of {@link org.springframework.data.mongodb.core.aggregation}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/package-info.java index 65522d8613..c5f5840e6b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/package-info.java @@ -3,6 +3,6 @@ * @deprecated since MongoDB server version 5.0 */ @Deprecated -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.mapreduce; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamRequest.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamRequest.java index fec7fa60ef..e1da0b33ce 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamRequest.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamRequest.java @@ -20,12 +20,13 @@ import org.bson.BsonValue; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.ChangeStreamOptions; import org.springframework.data.mongodb.core.ChangeStreamOptions.ChangeStreamOptionsBuilder; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.messaging.ChangeStreamRequest.ChangeStreamRequestOptions; import org.springframework.data.mongodb.core.query.Collation; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import com.mongodb.client.model.changestream.ChangeStreamDocument; @@ -215,12 +216,12 @@ public ChangeStreamOptions getChangeStreamOptions() { } @Override - public String getCollectionName() { + public @Nullable String getCollectionName() { return collectionName; } @Override - public String getDatabaseName() { + public @Nullable String getDatabaseName() { return databaseName; } @@ -253,6 +254,7 @@ private ChangeStreamRequestBuilder() {} * @param databaseName must not be {@literal null} nor empty. * @return this. */ + @Contract("_ -> this") public ChangeStreamRequestBuilder database(String databaseName) { Assert.hasText(databaseName, "DatabaseName must not be null"); @@ -267,6 +269,7 @@ public ChangeStreamRequestBuilder database(String databaseName) { * @param collectionName must not be {@literal null} nor empty. * @return this. */ + @Contract("_ -> this") public ChangeStreamRequestBuilder collection(String collectionName) { Assert.hasText(collectionName, "CollectionName must not be null"); @@ -281,6 +284,7 @@ public ChangeStreamRequestBuilder collection(String collectionName) { * @param messageListener must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public ChangeStreamRequestBuilder publishTo( MessageListener, ? super T> messageListener) { @@ -308,6 +312,7 @@ public ChangeStreamRequestBuilder publishTo( * @see ChangeStreamOptions#getFilter() * @see ChangeStreamOptionsBuilder#filter(Aggregation) */ + @Contract("_ -> this") public ChangeStreamRequestBuilder filter(Aggregation aggregation) { Assert.notNull(aggregation, "Aggregation must not be null"); @@ -323,6 +328,7 @@ public ChangeStreamRequestBuilder filter(Aggregation aggregation) { * @return this. * @see ChangeStreamOptions#getFilter() */ + @Contract("_ -> this") public ChangeStreamRequestBuilder filter(Document... pipeline) { Assert.notNull(pipeline, "Aggregation pipeline must not be null"); @@ -340,6 +346,7 @@ public ChangeStreamRequestBuilder filter(Document... pipeline) { * @see ChangeStreamOptions#getCollation() * @see ChangeStreamOptionsBuilder#collation(Collation) */ + @Contract("_ -> this") public ChangeStreamRequestBuilder collation(Collation collation) { Assert.notNull(collation, "Collation must not be null"); @@ -357,6 +364,7 @@ public ChangeStreamRequestBuilder collation(Collation collation) { * @see ChangeStreamOptions#getResumeToken() * @see ChangeStreamOptionsBuilder#resumeToken(org.bson.BsonValue) */ + @Contract("_ -> this") public ChangeStreamRequestBuilder resumeToken(BsonValue resumeToken) { Assert.notNull(resumeToken, "Resume token not be null"); @@ -373,6 +381,7 @@ public ChangeStreamRequestBuilder resumeToken(BsonValue resumeToken) { * @see ChangeStreamOptions#getResumeTimestamp() * @see ChangeStreamOptionsBuilder#resumeAt(java.time.Instant) */ + @Contract("_ -> this") public ChangeStreamRequestBuilder resumeAt(Instant clusterTime) { Assert.notNull(clusterTime, "ClusterTime must not be null"); @@ -388,6 +397,7 @@ public ChangeStreamRequestBuilder resumeAt(Instant clusterTime) { * @return this. * @since 2.2 */ + @Contract("_ -> this") public ChangeStreamRequestBuilder resumeAfter(BsonValue resumeToken) { Assert.notNull(resumeToken, "ResumeToken must not be null"); @@ -403,6 +413,7 @@ public ChangeStreamRequestBuilder resumeAfter(BsonValue resumeToken) { * @return this. * @since 2.2 */ + @Contract("_ -> this") public ChangeStreamRequestBuilder startAfter(BsonValue resumeToken) { Assert.notNull(resumeToken, "ResumeToken must not be null"); @@ -418,6 +429,7 @@ public ChangeStreamRequestBuilder startAfter(BsonValue resumeToken) { * @see ChangeStreamOptions#getFullDocumentLookup() * @see ChangeStreamOptionsBuilder#fullDocumentLookup(FullDocument) */ + @Contract("_ -> this") public ChangeStreamRequestBuilder fullDocumentLookup(FullDocument lookup) { Assert.notNull(lookup, "FullDocument not be null"); @@ -434,6 +446,7 @@ public ChangeStreamRequestBuilder fullDocumentLookup(FullDocument lookup) { * @see ChangeStreamOptions#getFullDocumentBeforeChangeLookup() * @see ChangeStreamOptionsBuilder#fullDocumentBeforeChangeLookup(FullDocumentBeforeChange) */ + @Contract("_ -> this") public ChangeStreamRequestBuilder fullDocumentBeforeChangeLookup(FullDocumentBeforeChange lookup) { Assert.notNull(lookup, "FullDocumentBeforeChange not be null"); @@ -448,6 +461,7 @@ public ChangeStreamRequestBuilder fullDocumentBeforeChangeLookup(FullDocument * @param timeout must not be {@literal null}. * @since 3.0 */ + @Contract("_ -> this") public ChangeStreamRequestBuilder maxAwaitTime(Duration timeout) { Assert.notNull(timeout, "timeout not be null"); @@ -459,6 +473,7 @@ public ChangeStreamRequestBuilder maxAwaitTime(Duration timeout) { /** * @return the build {@link ChangeStreamRequest}. */ + @Contract("-> new") public ChangeStreamRequest build() { Assert.notNull(listener, "MessageListener must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamTask.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamTask.java index fc8372613b..cc4d3f0bdb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamTask.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamTask.java @@ -27,6 +27,7 @@ import org.bson.BsonTimestamp; import org.bson.BsonValue; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.ChangeStreamEvent; import org.springframework.data.mongodb.core.ChangeStreamOptions; import org.springframework.data.mongodb.core.MongoTemplate; @@ -39,7 +40,6 @@ import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.messaging.Message.MessageProperties; import org.springframework.data.mongodb.core.messaging.SubscriptionRequest.RequestOptions; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ErrorHandler; import org.springframework.util.StringUtils; @@ -224,21 +224,18 @@ static class ChangeStreamEventMessage implements Message getRaw() { + public @Nullable ChangeStreamDocument getRaw() { return delegate.getRaw(); } - @Nullable @Override - public T getBody() { + public @Nullable T getBody() { return delegate.getBody(); } - @Nullable @Override - public T getBodyBeforeChange() { + public @Nullable T getBodyBeforeChange() { return delegate.getBodyBeforeChange(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/CursorReadingTask.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/CursorReadingTask.java index 41b5fed4f5..662960284d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/CursorReadingTask.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/CursorReadingTask.java @@ -21,12 +21,12 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.messaging.Message.MessageProperties; import org.springframework.data.mongodb.core.messaging.SubscriptionRequest.RequestOptions; import org.springframework.data.util.Lock; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ErrorHandler; @@ -51,7 +51,7 @@ abstract class CursorReadingTask implements Task { private State state = State.CREATED; - private MongoCursor cursor; + private @Nullable MongoCursor cursor; /** * @param template must not be {@literal null}. @@ -109,6 +109,7 @@ public void run() { * is immediately {@link MongoCursor#close() closed} and a new {@link MongoCursor} is requested until a valid one is * retrieved or the {@link #state} changes. */ + @SuppressWarnings("NullAway") private void start() { lock.executeWithoutResult(() -> { @@ -188,6 +189,7 @@ public boolean awaitStart(Duration timeout) throws InterruptedException { return awaitStart.await(timeout.toNanos(), TimeUnit.NANOSECONDS); } + @SuppressWarnings("NullAway") protected Message createMessage(T source, Class targetType, RequestOptions options) { SimpleMessage message = new SimpleMessage<>(source, source, MessageProperties.builder() @@ -209,11 +211,10 @@ private void emitMessage(Message message) { } } - @Nullable - private T getNext() { + private @Nullable T getNext() { return lock.execute(() -> { - if (State.RUNNING.equals(state)) { + if (cursor != null && State.RUNNING.equals(state)) { return cursor.tryNext(); } throw new IllegalStateException(String.format("Cursor %s is not longer open", cursor)); @@ -239,8 +240,7 @@ private static boolean isValidCursor(@Nullable MongoCursor cursor) { * @return can be {@literal null}. * @throws RuntimeException The potentially translated exception. */ - @Nullable - private V execute(Supplier callback) { + private @Nullable V execute(Supplier callback) { try { return callback.get(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/DefaultMessageListenerContainer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/DefaultMessageListenerContainer.java index 546f3fdd33..1b24e67e07 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/DefaultMessageListenerContainer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/DefaultMessageListenerContainer.java @@ -25,12 +25,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.messaging.SubscriptionRequest.RequestOptions; import org.springframework.data.util.Lock; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ErrorHandler; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/LazyMappingDelegatingMessage.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/LazyMappingDelegatingMessage.java index 1c934e8302..f9a9c4131d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/LazyMappingDelegatingMessage.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/LazyMappingDelegatingMessage.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core.messaging; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.util.ClassUtils; @@ -38,12 +39,12 @@ class LazyMappingDelegatingMessage implements Message { } @Override - public S getRaw() { + public @Nullable S getRaw() { return delegate.getRaw(); } @Override - public T getBody() { + public @Nullable T getBody() { if (delegate.getBody() == null || targetType.equals(delegate.getBody().getClass())) { return targetType.cast(delegate.getBody()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/Message.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/Message.java index 46db068096..e7aa5b036d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/Message.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/Message.java @@ -15,7 +15,8 @@ */ package org.springframework.data.mongodb.core.messaging; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -59,8 +60,7 @@ public interface Message { * @return can be {@literal null}. * @since 4.0 */ - @Nullable - default T getBodyBeforeChange() { + default @Nullable T getBodyBeforeChange() { return null; } @@ -87,8 +87,7 @@ class MessageProperties { * * @return can be {@literal null}. */ - @Nullable - public String getDatabaseName() { + public @Nullable String getDatabaseName() { return databaseName; } @@ -97,8 +96,7 @@ public String getDatabaseName() { * * @return can be {@literal null}. */ - @Nullable - public String getCollectionName() { + public @Nullable String getCollectionName() { return collectionName; } @@ -162,6 +160,7 @@ public static class MessagePropertiesBuilder { * @param dbName must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MessagePropertiesBuilder databaseName(String dbName) { Assert.notNull(dbName, "Database name must not be null"); @@ -174,6 +173,7 @@ public MessagePropertiesBuilder databaseName(String dbName) { * @param collectionName must not be {@literal null}. * @return this */ + @Contract("_ -> this") public MessagePropertiesBuilder collectionName(String collectionName) { Assert.notNull(collectionName, "Collection name must not be null"); @@ -185,6 +185,7 @@ public MessagePropertiesBuilder collectionName(String collectionName) { /** * @return the built {@link MessageProperties}. */ + @Contract("-> new") public MessageProperties build() { MessageProperties properties = new MessageProperties(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SimpleMessage.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SimpleMessage.java index be5308e3cf..acb7bfd8a2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SimpleMessage.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SimpleMessage.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.messaging; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -46,12 +46,12 @@ class SimpleMessage implements Message { } @Override - public S getRaw() { + public @Nullable S getRaw() { return raw; } @Override - public T getBody() { + public @Nullable T getBody() { return body; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SubscriptionRequest.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SubscriptionRequest.java index 287ba293b6..7b914f16f5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SubscriptionRequest.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SubscriptionRequest.java @@ -17,9 +17,9 @@ import java.time.Duration; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.messaging.SubscriptionRequest.RequestOptions; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -61,8 +61,7 @@ interface RequestOptions { * @return the name of the database to subscribe to. Can be {@literal null} in which case the default * {@link MongoDatabaseFactory#getMongoDatabase() database} is used. */ - @Nullable - default String getDatabaseName() { + default @Nullable String getDatabaseName() { return null; } @@ -106,7 +105,7 @@ static RequestOptions justDatabase(String database) { return new RequestOptions() { @Override - public String getCollectionName() { + public @Nullable String getCollectionName() { return null; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/TailableCursorRequest.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/TailableCursorRequest.java index c6caef12fb..92e23ff847 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/TailableCursorRequest.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/TailableCursorRequest.java @@ -18,10 +18,11 @@ import java.util.Optional; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.messaging.SubscriptionRequest.RequestOptions; import org.springframework.data.mongodb.core.messaging.TailableCursorRequest.TailableCursorRequestOptions.TailableCursorRequestOptionsBuilder; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -121,6 +122,7 @@ public static class TailableCursorRequestOptions implements SubscriptionRequest. TailableCursorRequestOptions() {} + @SuppressWarnings("NullAway") public static TailableCursorRequestOptions of(RequestOptions options) { return builder().collection(options.getCollectionName()).build(); } @@ -136,7 +138,7 @@ public static TailableCursorRequestOptionsBuilder builder() { } @Override - public String getCollectionName() { + public @Nullable String getCollectionName() { return collectionName; } @@ -163,6 +165,7 @@ private TailableCursorRequestOptionsBuilder() {} * @param collection must not be {@literal null} nor {@literal empty}. * @return this. */ + @Contract("_ -> this") public TailableCursorRequestOptionsBuilder collection(String collection) { Assert.hasText(collection, "Collection must not be null nor empty"); @@ -177,6 +180,7 @@ public TailableCursorRequestOptionsBuilder collection(String collection) { * @param filter the {@link Query } to apply for filtering events. Must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public TailableCursorRequestOptionsBuilder filter(Query filter) { Assert.notNull(filter, "Filter must not be null"); @@ -188,6 +192,7 @@ public TailableCursorRequestOptionsBuilder filter(Query filter) { /** * @return the built {@link TailableCursorRequestOptions}. */ + @Contract("-> new") public TailableCursorRequestOptions build() { TailableCursorRequestOptions options = new TailableCursorRequestOptions(); @@ -220,6 +225,7 @@ private TailableCursorRequestBuilder() {} * @param collectionName must not be {@literal null} nor empty. * @return this. */ + @Contract("_ -> this") public TailableCursorRequestBuilder collection(String collectionName) { Assert.hasText(collectionName, "CollectionName must not be null"); @@ -234,6 +240,7 @@ public TailableCursorRequestBuilder collection(String collectionName) { * @param messageListener must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public TailableCursorRequestBuilder publishTo(MessageListener messageListener) { Assert.notNull(messageListener, "MessageListener must not be null"); @@ -248,6 +255,7 @@ public TailableCursorRequestBuilder publishTo(MessageListener this") public TailableCursorRequestBuilder filter(Query filter) { Assert.notNull(filter, "Filter must not be null"); @@ -259,6 +267,7 @@ public TailableCursorRequestBuilder filter(Query filter) { /** * @return the build {@link ChangeStreamRequest}. */ + @Contract("_ -> new") public TailableCursorRequest build() { Assert.notNull(listener, "MessageListener must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/package-info.java index 35be8f2ef8..aa879cc3c3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/package-info.java @@ -2,5 +2,5 @@ * MongoDB specific messaging support for listening to eg. * Change Streams. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.messaging; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/package-info.java index e2f9169d0d..cae1d3df48 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/package-info.java @@ -1,6 +1,6 @@ /** * MongoDB core support. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicQuery.java index 8b1620b320..fd81030275 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicQuery.java @@ -18,7 +18,8 @@ import static org.springframework.util.ObjectUtils.*; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -91,6 +92,7 @@ public BasicQuery(Document queryObject, Document fieldsObject) { * @param query the query to copy. * @since 4.4 */ + @SuppressWarnings("NullAway") public BasicQuery(Query query) { super(query); @@ -101,6 +103,7 @@ public BasicQuery(Query query) { } @Override + @Contract("_ -> this") public Query addCriteria(CriteriaDefinition criteria) { this.queryObject.putAll(criteria.getCriteriaObject()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicUpdate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicUpdate.java index 12843ce622..3d89f1e1b7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicUpdate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicUpdate.java @@ -23,13 +23,11 @@ import java.util.function.BiFunction; import org.bson.Document; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.ClassUtils; /** - * {@link Document}-based {@link Update} variant. - * * @author Thomas Risberg * @author John Brisbin * @author Oliver Gierke @@ -49,48 +47,56 @@ public BasicUpdate(Document updateObject) { } @Override + @Contract("_, _ -> this") public Update set(String key, @Nullable Object value) { setOperationValue("$set", key, value); return this; } @Override + @Contract("_ -> this") public Update unset(String key) { setOperationValue("$unset", key, 1); return this; } @Override + @Contract("_, _ -> this") public Update inc(String key, Number inc) { setOperationValue("$inc", key, inc); return this; } @Override + @Contract("_, _ -> this") public Update push(String key, @Nullable Object value) { setOperationValue("$push", key, value); return this; } @Override + @Contract("_, _ -> this") public Update addToSet(String key, @Nullable Object value) { setOperationValue("$addToSet", key, value); return this; } @Override + @Contract("_, _ -> this") public Update pop(String key, Position pos) { setOperationValue("$pop", key, (pos == Position.FIRST ? -1 : 1)); return this; } @Override + @Contract("_, _ -> this") public Update pull(String key, @Nullable Object value) { setOperationValue("$pull", key, value); return this; } @Override + @Contract("_, _ -> this") public Update pullAll(String key, Object[] values) { setOperationValue("$pullAll", key, List.of(values), (o, o2) -> { @@ -107,6 +113,7 @@ public Update pullAll(String key, Object[] values) { } @Override + @Contract("_, _ -> this") public Update rename(String oldName, String newName) { setOperationValue("$rename", oldName, newName); return this; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Collation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Collation.java index de24c0511d..217e669883 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Collation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Collation.java @@ -19,8 +19,9 @@ import java.util.Optional; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -180,6 +181,7 @@ public static Collation from(Document source) { * @param strength comparison level. * @return new {@link Collation}. */ + @Contract("_ -> new") public Collation strength(int strength) { ComparisonLevel current = this.strength.orElseGet(() -> new ICUComparisonLevel(strength)); @@ -192,6 +194,7 @@ public Collation strength(int strength) { * @param comparisonLevel must not be {@literal null}. * @return new {@link Collation} */ + @Contract("_ -> new") public Collation strength(ComparisonLevel comparisonLevel) { Collation newInstance = copy(); @@ -205,6 +208,7 @@ public Collation strength(ComparisonLevel comparisonLevel) { * @param caseLevel use {@literal true} to enable {@code caseLevel} comparison. * @return new {@link Collation}. */ + @Contract("_ -> new") public Collation caseLevel(boolean caseLevel) { ComparisonLevel strengthValue = strength.orElseGet(ComparisonLevel::primary); @@ -218,6 +222,7 @@ public Collation caseLevel(boolean caseLevel) { * @param caseFirst must not be {@literal null}. * @return new instance of {@link Collation}. */ + @Contract("_ -> new") public Collation caseFirst(String caseFirst) { return caseFirst(new CaseFirst(caseFirst)); } @@ -228,6 +233,7 @@ public Collation caseFirst(String caseFirst) { * @param sort must not be {@literal null}. * @return new instance of {@link Collation}. */ + @Contract("_ -> new") public Collation caseFirst(CaseFirst sort) { ComparisonLevel strengthValue = strength.orElseGet(ComparisonLevel::tertiary); @@ -239,6 +245,7 @@ public Collation caseFirst(CaseFirst sort) { * * @return new {@link Collation}. */ + @Contract("-> new") public Collation numericOrderingEnabled() { return numericOrdering(true); } @@ -248,6 +255,7 @@ public Collation numericOrderingEnabled() { * * @return new {@link Collation}. */ + @Contract("-> new") public Collation numericOrderingDisabled() { return numericOrdering(false); } @@ -257,6 +265,7 @@ public Collation numericOrderingDisabled() { * * @return new {@link Collation}. */ + @Contract("_ -> new") public Collation numericOrdering(boolean flag) { Collation newInstance = copy(); @@ -271,6 +280,7 @@ public Collation numericOrdering(boolean flag) { * @param alternate must not be {@literal null}. * @return new {@link Collation}. */ + @Contract("_ -> new") public Collation alternate(String alternate) { Alternate instance = this.alternate.orElseGet(() -> new Alternate(alternate, Optional.empty())); @@ -284,6 +294,7 @@ public Collation alternate(String alternate) { * @param alternate must not be {@literal null}. * @return new {@link Collation}. */ + @Contract("_ -> new") public Collation alternate(Alternate alternate) { Collation newInstance = copy(); @@ -296,6 +307,7 @@ public Collation alternate(Alternate alternate) { * * @return new {@link Collation}. */ + @Contract("_ -> new") public Collation backwardDiacriticSort() { return backwards(true); } @@ -305,6 +317,7 @@ public Collation backwardDiacriticSort() { * * @return new {@link Collation}. */ + @Contract("-> new") public Collation forwardDiacriticSort() { return backwards(false); } @@ -315,6 +328,7 @@ public Collation forwardDiacriticSort() { * @param backwards must not be {@literal null}. * @return new {@link Collation}. */ + @Contract("_ -> new") public Collation backwards(boolean backwards) { Collation newInstance = copy(); @@ -327,6 +341,7 @@ public Collation backwards(boolean backwards) { * * @return new {@link Collation}. */ + @Contract("-> new") public Collation normalizationEnabled() { return normalization(true); } @@ -336,6 +351,7 @@ public Collation normalizationEnabled() { * * @return new {@link Collation}. */ + @Contract("-> new") public Collation normalizationDisabled() { return normalization(false); } @@ -346,6 +362,7 @@ public Collation normalizationDisabled() { * @param normalization must not be {@literal null}. * @return new {@link Collation}. */ + @Contract("_ -> new") public Collation normalization(boolean normalization) { Collation newInstance = copy(); @@ -359,6 +376,7 @@ public Collation normalization(boolean normalization) { * @param maxVariable must not be {@literal null}. * @return new {@link Collation}. */ + @Contract("_ -> new") public Collation maxVariable(String maxVariable) { Alternate alternateValue = alternate.orElseGet(Alternate::shifted); @@ -370,6 +388,7 @@ public Collation maxVariable(String maxVariable) { * * @return the native MongoDB {@link Document} representation of the {@link Collation}. */ + @SuppressWarnings("NullAway") public Document toDocument() { return map(toMongoDocumentConverter()); } @@ -379,7 +398,7 @@ public Document toDocument() { * * @return he native MongoDB representation of the {@link Collation}. */ - public com.mongodb.client.model.Collation toMongoCollation() { + public com.mongodb.client.model.@Nullable Collation toMongoCollation() { return map(toMongoCollationConverter()); } @@ -390,7 +409,7 @@ public com.mongodb.client.model.Collation toMongoCollation() { * @param * @return the converted result. */ - public R map(Converter mapper) { + public @Nullable R map(Converter mapper) { return mapper.convert(this); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java index 8d4cb703bb..d25b98ab1a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.query; -import static org.springframework.util.ObjectUtils.*; +import static org.springframework.util.ObjectUtils.nullSafeHashCode; import java.util.ArrayList; import java.util.Arrays; @@ -33,6 +33,7 @@ import org.bson.BsonType; import org.bson.Document; import org.bson.types.Binary; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Example; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Point; @@ -45,7 +46,7 @@ import org.springframework.data.mongodb.core.schema.JsonSchemaProperty; import org.springframework.data.mongodb.core.schema.MongoJsonSchema; import org.springframework.data.mongodb.util.RegexFlags; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; @@ -184,6 +185,7 @@ public static Criteria expr(MongoExpression expression) { * * @return new instance of {@link Criteria}. */ + @Contract("_ -> new") public Criteria and(String key) { return new Criteria(this.criteriaChain, key); } @@ -194,6 +196,7 @@ public Criteria and(String key) { * @param value can be {@literal null}. * @return this. */ + @Contract("_ -> this") public Criteria is(@Nullable Object value) { if (!NOT_SET.equals(isValue)) { @@ -221,6 +224,7 @@ public Criteria is(@Nullable Object value) { * Missing Fields: Equality Filter * @since 3.3 */ + @Contract("_ -> this") public Criteria isNull() { return is(null); } @@ -237,6 +241,7 @@ public Criteria isNull() { * Fields: Type Check * @since 3.3 */ + @Contract("_ -> this") public Criteria isNullValue() { criteria.put("$type", BsonType.NULL.getValue()); @@ -254,6 +259,7 @@ private boolean lastOperatorWasNot() { * @return this. * @see MongoDB Query operator: $ne */ + @Contract("_ -> this") public Criteria ne(@Nullable Object value) { criteria.put("$ne", value); return this; @@ -266,6 +272,7 @@ public Criteria ne(@Nullable Object value) { * @return this. * @see MongoDB Query operator: $lt */ + @Contract("_ -> this") public Criteria lt(Object value) { criteria.put("$lt", value); return this; @@ -278,6 +285,7 @@ public Criteria lt(Object value) { * @return this. * @see MongoDB Query operator: $lte */ + @Contract("_ -> this") public Criteria lte(Object value) { criteria.put("$lte", value); return this; @@ -290,6 +298,7 @@ public Criteria lte(Object value) { * @return this. * @see MongoDB Query operator: $gt */ + @Contract("_ -> this") public Criteria gt(Object value) { criteria.put("$gt", value); return this; @@ -302,6 +311,7 @@ public Criteria gt(Object value) { * @return this. * @see MongoDB Query operator: $gte */ + @Contract("_ -> this") public Criteria gte(Object value) { criteria.put("$gte", value); return this; @@ -314,13 +324,13 @@ public Criteria gte(Object value) { * @return this. * @see MongoDB Query operator: $in */ - public Criteria in(Object... values) { + @Contract("_ -> this") + public Criteria in(@Nullable Object ... values) { if (values.length > 1 && values[1] instanceof Collection) { throw new InvalidMongoDbApiUsageException( "You can only pass in one argument of type " + values[1].getClass().getName()); } - criteria.put("$in", Arrays.asList(values)); - return this; + return this.in(Arrays.asList(values)); } /** @@ -330,8 +340,15 @@ public Criteria in(Object... values) { * @return this. * @see MongoDB Query operator: $in */ + @Contract("_ -> this") public Criteria in(Collection values) { - criteria.put("$in", values); + + ArrayList objects = new ArrayList<>(values); + if (objects.size() == 1 && CollectionUtils.firstElement(objects) instanceof Placeholder placeholder) { + criteria.put("$in", placeholder); + } else { + criteria.put("$in", objects); + } return this; } @@ -342,6 +359,7 @@ public Criteria in(Collection values) { * @return this. * @see MongoDB Query operator: $nin */ + @Contract("_ -> this") public Criteria nin(Object... values) { return nin(Arrays.asList(values)); } @@ -353,8 +371,15 @@ public Criteria nin(Object... values) { * @return this. * @see MongoDB Query operator: $nin */ + @Contract("_ -> this") public Criteria nin(Collection values) { - criteria.put("$nin", values); + + ArrayList objects = new ArrayList<>(values); + if (objects.size() == 1 && CollectionUtils.firstElement(objects) instanceof Placeholder placeholder) { + criteria.put("$nin", placeholder); + } else { + criteria.put("$nin", objects); + } return this; } @@ -366,6 +391,7 @@ public Criteria nin(Collection values) { * @return this. * @see MongoDB Query operator: $mod */ + @Contract("_ -> this") public Criteria mod(Number value, Number remainder) { List l = new ArrayList<>(2); l.add(value); @@ -381,6 +407,7 @@ public Criteria mod(Number value, Number remainder) { * @return this. * @see MongoDB Query operator: $all */ + @Contract("_ -> this") public Criteria all(Object... values) { return all(Arrays.asList(values)); } @@ -392,6 +419,7 @@ public Criteria all(Object... values) { * @return this. * @see MongoDB Query operator: $all */ + @Contract("_ -> this") public Criteria all(Collection values) { criteria.put("$all", values); return this; @@ -404,6 +432,7 @@ public Criteria all(Collection values) { * @return this. * @see MongoDB Query operator: $size */ + @Contract("_ -> this") public Criteria size(int size) { criteria.put("$size", size); return this; @@ -416,6 +445,7 @@ public Criteria size(int size) { * @return this. * @see MongoDB Query operator: $exists */ + @Contract("_ -> this") public Criteria exists(boolean value) { criteria.put("$exists", value); return this; @@ -431,6 +461,7 @@ public Criteria exists(boolean value) { * $sampleRate * @since 3.3 */ + @Contract("_ -> this") public Criteria sampleRate(double sampleRate) { Assert.isTrue(sampleRate >= 0, "The sample rate must be greater than zero"); @@ -447,6 +478,7 @@ public Criteria sampleRate(double sampleRate) { * @return this. * @see MongoDB Query operator: $type */ + @Contract("_ -> this") public Criteria type(int typeNumber) { criteria.put("$type", typeNumber); return this; @@ -460,6 +492,7 @@ public Criteria type(int typeNumber) { * @since 2.1 * @see MongoDB Query operator: $type */ + @Contract("_ -> this") public Criteria type(Type... types) { Assert.notNull(types, "Types must not be null"); @@ -476,6 +509,7 @@ public Criteria type(Type... types) { * @since 3.2 * @see MongoDB Query operator: $type */ + @Contract("_ -> this") public Criteria type(Collection types) { Assert.notNull(types, "Types must not be null"); @@ -490,6 +524,7 @@ public Criteria type(Collection types) { * @return this. * @see MongoDB Query operator: $not */ + @Contract("-> this") public Criteria not() { return not(null); } @@ -501,6 +536,7 @@ public Criteria not() { * @return this. * @see MongoDB Query operator: $not */ + @Contract("_ -> this") private Criteria not(@Nullable Object value) { criteria.put("$not", value); return this; @@ -513,6 +549,7 @@ private Criteria not(@Nullable Object value) { * @return this. * @see MongoDB Query operator: $regex */ + @Contract("_ -> this") public Criteria regex(String regex) { return regex(regex, null); } @@ -525,6 +562,7 @@ public Criteria regex(String regex) { * @return this. * @see MongoDB Query operator: $regex */ + @Contract("_, _ -> this") public Criteria regex(String regex, @Nullable String options) { return regex(toPattern(regex, options)); } @@ -535,6 +573,7 @@ public Criteria regex(String regex, @Nullable String options) { * @param pattern must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Criteria regex(Pattern pattern) { Assert.notNull(pattern, "Pattern must not be null"); @@ -553,6 +592,7 @@ public Criteria regex(Pattern pattern) { * @param regex must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Criteria regex(BsonRegularExpression regex) { if (lastOperatorWasNot()) { @@ -581,6 +621,7 @@ private Pattern toPattern(String regex, @Nullable String options) { * @see MongoDB Query operator: * $centerSphere */ + @Contract("_ -> this") public Criteria withinSphere(Circle circle) { Assert.notNull(circle, "Circle must not be null"); @@ -597,6 +638,7 @@ public Criteria withinSphere(Circle circle) { * @see MongoDB Query operator: * $geoWithin */ + @Contract("_ -> this") public Criteria within(Shape shape) { Assert.notNull(shape, "Shape must not be null"); @@ -612,6 +654,7 @@ public Criteria within(Shape shape) { * @return this. * @see MongoDB Query operator: $near */ + @Contract("_ -> this") public Criteria near(Point point) { Assert.notNull(point, "Point must not be null"); @@ -629,6 +672,7 @@ public Criteria near(Point point) { * @see MongoDB Query operator: * $nearSphere */ + @Contract("_ -> this") public Criteria nearSphere(Point point) { Assert.notNull(point, "Point must not be null"); @@ -646,6 +690,7 @@ public Criteria nearSphere(Point point) { * @since 1.8 */ @SuppressWarnings("rawtypes") + @Contract("_ -> this") public Criteria intersects(GeoJson geoJson) { Assert.notNull(geoJson, "GeoJson must not be null"); @@ -665,6 +710,7 @@ public Criteria intersects(GeoJson geoJson) { * @see MongoDB Query operator: * $maxDistance */ + @Contract("_ -> this") public Criteria maxDistance(double maxDistance) { if (createNearCriteriaForCommand("$near", "$maxDistance", maxDistance) @@ -687,6 +733,7 @@ public Criteria maxDistance(double maxDistance) { * @return this. * @since 1.7 */ + @Contract("_ -> this") public Criteria minDistance(double minDistance) { if (createNearCriteriaForCommand("$near", "$minDistance", minDistance) @@ -706,6 +753,7 @@ public Criteria minDistance(double minDistance) { * @see MongoDB Query operator: * $elemMatch */ + @Contract("_ -> this") public Criteria elemMatch(Criteria criteria) { this.criteria.put("$elemMatch", criteria.getCriteriaObject()); return this; @@ -718,6 +766,7 @@ public Criteria elemMatch(Criteria criteria) { * @return this. * @since 1.8 */ + @Contract("_ -> this") public Criteria alike(Example sample) { if (StringUtils.hasText(this.getKey())) { @@ -745,6 +794,7 @@ public Criteria alike(Example sample) { * @see MongoDB Query operator: * $jsonSchema */ + @Contract("_ -> this") public Criteria andDocumentStructureMatches(MongoJsonSchema schema) { Assert.notNull(schema, "Schema must not be null"); @@ -776,6 +826,7 @@ public BitwiseCriteriaOperators bits() { * @param criteria must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Criteria orOperator(Criteria... criteria) { Assert.notNull(criteria, "Criteria must not be null"); @@ -793,6 +844,7 @@ public Criteria orOperator(Criteria... criteria) { * @return this. * @since 3.2 */ + @Contract("_ -> this") public Criteria orOperator(Collection criteria) { Assert.notNull(criteria, "Criteria must not be null"); @@ -810,6 +862,7 @@ public Criteria orOperator(Collection criteria) { * @param criteria must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Criteria norOperator(Criteria... criteria) { Assert.notNull(criteria, "Criteria must not be null"); @@ -827,6 +880,7 @@ public Criteria norOperator(Criteria... criteria) { * @return this. * @since 3.2 */ + @Contract("_ -> this") public Criteria norOperator(Collection criteria) { Assert.notNull(criteria, "Criteria must not be null"); @@ -844,6 +898,7 @@ public Criteria norOperator(Collection criteria) { * @param criteria must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Criteria andOperator(Criteria... criteria) { Assert.notNull(criteria, "Criteria must not be null"); @@ -861,6 +916,7 @@ public Criteria andOperator(Criteria... criteria) { * @return this. * @since 3.2 */ + @Contract("_ -> this") public Criteria andOperator(Collection criteria) { Assert.notNull(criteria, "Criteria must not be null"); @@ -869,6 +925,19 @@ public Criteria andOperator(Collection criteria) { return registerCriteriaChainElement(new Criteria("$and").is(bsonList)); } + /** + * Creates a criterion using the given {@literal operator}. + * + * @param operator the native MongoDB operator. + * @param value the operator value + * @return this + * @since 5.0 + */ + public Criteria raw(String operator, Object value) { + criteria.put(operator, value); + return this; + } + private Criteria registerCriteriaChainElement(Criteria criteria) { if (lastOperatorWasNot()) { @@ -884,8 +953,7 @@ private Criteria registerCriteriaChainElement(Criteria criteria) { * @see org.springframework.data.mongodb.core.query.CriteriaDefinition#getKey() */ @Override - @Nullable - public String getKey() { + public @Nullable String getKey() { return this.key; } @@ -900,7 +968,8 @@ public Document getCriteriaObject() { for (Criteria c : this.criteriaChain) { Document document = c.getSingleCriteriaObject(); for (String k : document.keySet()) { - setValue(criteriaObject, k, document.get(k)); + Object o = document.get(k); + setValue(criteriaObject, k, o); } } return criteriaObject; @@ -1095,7 +1164,7 @@ private boolean isEqual(@Nullable Object left, @Nullable Object right) { if (Collection.class.isAssignableFrom(left.getClass())) { - if (!Collection.class.isAssignableFrom(right.getClass())) { + if (right == null || !Collection.class.isAssignableFrom(right.getClass())) { return false; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java index c00b1d4b82..7777e5f554 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core.query; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * @author Oliver Gierke @@ -40,4 +40,41 @@ public interface CriteriaDefinition { @Nullable String getKey(); + /** + * A placeholder expression used when rending queries to JSON. + * + * @since 5.0 + * @author Christoph Strobl + */ + class Placeholder { + + private final Object expression; + + /** + * Create a new placeholder for index bindable parameter. + * + * @param position the index of the parameter to bind. + * @return new instance of {@link Placeholder}. + */ + public static Placeholder indexed(int position) { + return new Placeholder("?%s".formatted(position)); + } + + public static Placeholder placeholder(String expression) { + return new Placeholder(expression); + } + + Placeholder(Object value) { + this.expression = value; + } + + public Object getValue() { + return expression; + } + + @Override + public String toString() { + return getValue().toString(); + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Field.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Field.java index 3540a5a836..9775fefdb0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Field.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Field.java @@ -22,8 +22,9 @@ import java.util.Map.Entry; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.MongoExpression; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -52,6 +53,7 @@ public class Field { * @param field the document field name to be included. * @return {@code this} field projection instance. */ + @Contract("_ -> this") public Field include(String field) { Assert.notNull(field, "Key must not be null"); @@ -111,6 +113,7 @@ public FieldProjectionExpression project(MongoExpression expression) { * @return new instance of {@link FieldProjectionExpression}. * @since 3.2 */ + @Contract("_, _ -> this") public Field projectAs(MongoExpression expression, String field) { criteria.put(field, expression); @@ -124,6 +127,7 @@ public Field projectAs(MongoExpression expression, String field) { * @return {@code this} field projection instance. * @since 3.1 */ + @Contract("_ -> this") public Field include(String... fields) { return include(Arrays.asList(fields)); } @@ -135,6 +139,7 @@ public Field include(String... fields) { * @return {@code this} field projection instance. * @since 4.4 */ + @Contract("_ -> this") public Field include(Collection fields) { Assert.notNull(fields, "Keys must not be null"); @@ -149,6 +154,7 @@ public Field include(Collection fields) { * @param field the document field name to be excluded. * @return {@code this} field projection instance. */ + @Contract("_ -> this") public Field exclude(String field) { Assert.notNull(field, "Key must not be null"); @@ -165,6 +171,7 @@ public Field exclude(String field) { * @return {@code this} field projection instance. * @since 3.1 */ + @Contract("_ -> this") public Field exclude(String... fields) { return exclude(Arrays.asList(fields)); } @@ -176,6 +183,7 @@ public Field exclude(String... fields) { * @return {@code this} field projection instance. * @since 4.4 */ + @Contract("_ -> this") public Field exclude(Collection fields) { Assert.notNull(fields, "Keys must not be null"); @@ -191,6 +199,7 @@ public Field exclude(Collection fields) { * @param size the number of elements to include. * @return {@code this} field projection instance. */ + @Contract("_, _ -> this") public Field slice(String field, int size) { Assert.notNull(field, "Key must not be null"); @@ -209,12 +218,14 @@ public Field slice(String field, int size) { * @param size the number of elements to include. * @return {@code this} field projection instance. */ + @Contract("_, _, _ -> this") public Field slice(String field, int offset, int size) { slices.put(field, Arrays.asList(offset, size)); return this; } + @Contract("_, _ -> this") public Field elemMatch(String field, Criteria elemMatchCriteria) { elemMatches.put(field, elemMatchCriteria); @@ -229,6 +240,7 @@ public Field elemMatch(String field, Criteria elemMatchCriteria) { * @param value * @return {@code this} field projection instance. */ + @Contract("_, _ -> this") public Field position(String field, int value) { Assert.hasText(field, "DocumentField must not be null or empty"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/GeoCommand.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/GeoCommand.java index 83417c7200..19ecd94e23 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/GeoCommand.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/GeoCommand.java @@ -17,12 +17,12 @@ import static org.springframework.util.ObjectUtils.*; +import org.jspecify.annotations.Nullable; import org.springframework.data.geo.Box; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Polygon; import org.springframework.data.geo.Shape; import org.springframework.data.mongodb.core.geo.Sphere; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Meta.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Meta.java index 5757aa94a2..5ec4af3989 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Meta.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Meta.java @@ -23,7 +23,7 @@ import java.util.Map.Entry; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -50,8 +50,8 @@ private enum MetaKey { private Map values = Collections.emptyMap(); private Set flags = Collections.emptySet(); - private Integer cursorBatchSize; - private Boolean allowDiskUse; + private @Nullable Integer cursorBatchSize; + private @Nullable Boolean allowDiskUse; public Meta() {} @@ -85,8 +85,7 @@ public boolean hasMaxTime() { /** * @return {@literal null} if not set. */ - @Nullable - public Long getMaxTimeMsec() { + public @Nullable Long getMaxTimeMsec() { return getValue(MetaKey.MAX_TIME_MS.key); } @@ -181,8 +180,7 @@ public void setComment(String comment) { * @return {@literal null} if not set. * @since 2.1 */ - @Nullable - public Integer getCursorBatchSize() { + public @Nullable Integer getCursorBatchSize() { return cursorBatchSize; } @@ -285,9 +283,8 @@ void setValue(String key, @Nullable Object value) { this.values.put(key, value); } - @Nullable @SuppressWarnings("unchecked") - private T getValue(String key) { + private @Nullable T getValue(String key) { return (T) this.values.get(key); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java index 571bbd275c..5625de5e93 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java @@ -20,9 +20,11 @@ import java.math.MathContext; import java.math.RoundingMode; +import org.jspecify.annotations.Nullable; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metric; import org.springframework.data.geo.Metrics; +import org.springframework.util.Assert; /** * {@link Metric} and {@link Distance} conversions using the metric system. @@ -151,8 +153,8 @@ static ConversionMultiplierBuilder builder() { */ private static class ConversionMultiplierBuilder { - private Number from; - private Number to; + private @Nullable Number from; + private @Nullable Number to; ConversionMultiplierBuilder() {} @@ -177,6 +179,9 @@ ConversionMultiplierBuilder to(Metric to) { } ConversionMultiplier build() { + + Assert.notNull(from, "[From] must be set first"); + Assert.notNull(to, "[To] must be set first"); return new ConversionMultiplier(this.from, this.to); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java index e26a61c61e..b37c088981 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java @@ -18,7 +18,7 @@ import java.util.regex.Pattern; import org.bson.BsonRegularExpression; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * @author Christoph Strobl @@ -80,8 +80,7 @@ public enum MatchMode { * @param matcherType the type of matching to perform * @return {@literal source} when {@literal source} or {@literal matcherType} is {@literal null}. */ - @Nullable - public String toRegularExpression(@Nullable String source, @Nullable MatchMode matcherType) { + public @Nullable String toRegularExpression(@Nullable String source, @Nullable MatchMode matcherType) { if (matcherType == null || source == null) { return source; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java index f0f3b0a4dc..88d7dc5c1d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java @@ -18,6 +18,8 @@ import java.util.Arrays; import org.bson.Document; +import org.jspecify.annotations.Nullable; + import org.springframework.data.domain.Pageable; import org.springframework.data.geo.CustomMetric; import org.springframework.data.geo.Distance; @@ -27,7 +29,7 @@ import org.springframework.data.mongodb.core.ReadConcernAware; import org.springframework.data.mongodb.core.ReadPreferenceAware; import org.springframework.data.mongodb.core.geo.GeoJsonPoint; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -278,6 +280,7 @@ public Metric getMetric() { * @return * @since 2.2 */ + @Contract("_ -> this") public NearQuery limit(long limit) { this.limit = limit; return this; @@ -289,6 +292,7 @@ public NearQuery limit(long limit) { * @param skip * @return */ + @Contract("_ -> this") public NearQuery skip(long skip) { this.skip = skip; return this; @@ -300,6 +304,7 @@ public NearQuery skip(long skip) { * @param pageable must not be {@literal null} * @return */ + @Contract("_ -> this") public NearQuery with(Pageable pageable) { Assert.notNull(pageable, "Pageable must not be 'null'"); @@ -323,8 +328,9 @@ public NearQuery with(Pageable pageable) { * @param maxDistance * @return */ + @Contract("_ -> this") public NearQuery maxDistance(double maxDistance) { - return maxDistance(new Distance(maxDistance, getMetric())); + return maxDistance(Distance.of(maxDistance, getMetric())); } /** @@ -335,11 +341,12 @@ public NearQuery maxDistance(double maxDistance) { * @param metric must not be {@literal null}. * @return */ + @Contract("_, _ -> this") public NearQuery maxDistance(double maxDistance, Metric metric) { Assert.notNull(metric, "Metric must not be null"); - return maxDistance(new Distance(maxDistance, metric)); + return maxDistance(Distance.of(maxDistance, metric)); } /** @@ -349,6 +356,7 @@ public NearQuery maxDistance(double maxDistance, Metric metric) { * @param distance must not be {@literal null}. * @return */ + @Contract("_ -> this") public NearQuery maxDistance(Distance distance) { Assert.notNull(distance, "Distance must not be null"); @@ -379,8 +387,9 @@ public NearQuery maxDistance(Distance distance) { * @return * @since 1.7 */ + @Contract("_ -> this") public NearQuery minDistance(double minDistance) { - return minDistance(new Distance(minDistance, getMetric())); + return minDistance(Distance.of(minDistance, getMetric())); } /** @@ -392,11 +401,12 @@ public NearQuery minDistance(double minDistance) { * @return * @since 1.7 */ + @Contract("_, _ -> this") public NearQuery minDistance(double minDistance, Metric metric) { Assert.notNull(metric, "Metric must not be null"); - return minDistance(new Distance(minDistance, metric)); + return minDistance(Distance.of(minDistance, metric)); } /** @@ -407,6 +417,7 @@ public NearQuery minDistance(double minDistance, Metric metric) { * @return * @since 1.7 */ + @Contract("_ -> this") public NearQuery minDistance(Distance distance) { Assert.notNull(distance, "Distance must not be null"); @@ -428,8 +439,7 @@ public NearQuery minDistance(Distance distance) { * * @return */ - @Nullable - public Distance getMaxDistance() { + public @Nullable Distance getMaxDistance() { return this.maxDistance; } @@ -439,8 +449,7 @@ public Distance getMaxDistance() { * @return * @since 1.7 */ - @Nullable - public Distance getMinDistance() { + public @Nullable Distance getMinDistance() { return this.minDistance; } @@ -450,6 +459,7 @@ public Distance getMinDistance() { * @param distanceMultiplier * @return */ + @Contract("_ -> this") public NearQuery distanceMultiplier(double distanceMultiplier) { this.metric = new CustomMetric(distanceMultiplier); @@ -462,6 +472,7 @@ public NearQuery distanceMultiplier(double distanceMultiplier) { * @param spherical * @return */ + @Contract("_ -> this") public NearQuery spherical(boolean spherical) { this.spherical = spherical; return this; @@ -482,6 +493,7 @@ public boolean isSpherical() { * * @return */ + @Contract("-> this") public NearQuery inKilometers() { return adaptMetric(Metrics.KILOMETERS); } @@ -492,6 +504,7 @@ public NearQuery inKilometers() { * * @return */ + @Contract("-> this") public NearQuery inMiles() { return adaptMetric(Metrics.MILES); } @@ -504,6 +517,7 @@ public NearQuery inMiles() { * passed. * @return */ + @Contract("_ -> this") public NearQuery in(@Nullable Metric metric) { return adaptMetric(metric == null ? Metrics.NEUTRAL : metric); } @@ -514,6 +528,7 @@ public NearQuery in(@Nullable Metric metric) { * * @param metric */ + @Contract("_ -> this") private NearQuery adaptMetric(Metric metric) { if (metric != Metrics.NEUTRAL) { @@ -530,6 +545,7 @@ private NearQuery adaptMetric(Metric metric) { * @param query must not be {@literal null}. * @return */ + @Contract("_ -> this") public NearQuery query(Query query) { Assert.notNull(query, "Cannot apply 'null' query on NearQuery"); @@ -546,8 +562,7 @@ public NearQuery query(Query query) { /** * @return the number of elements to skip. */ - @Nullable - public Long getSkip() { + public @Nullable Long getSkip() { return skip; } @@ -557,8 +572,7 @@ public Long getSkip() { * @return the {@link Collation} if set. {@literal null} otherwise. * @since 2.2 */ - @Nullable - public Collation getCollation() { + public @Nullable Collation getCollation() { return query != null ? query.getCollation().orElse(null) : null; } @@ -570,6 +584,7 @@ public Collation getCollation() { * @return this. * @since 4.1 */ + @Contract("_ -> this") public NearQuery withReadConcern(ReadConcern readConcern) { Assert.notNull(readConcern, "ReadConcern must not be null"); @@ -585,6 +600,7 @@ public NearQuery withReadConcern(ReadConcern readConcern) { * @return this. * @since 4.1 */ + @Contract("_ -> this") public NearQuery withReadPreference(ReadPreference readPreference) { Assert.notNull(readPreference, "ReadPreference must not be null"); @@ -596,14 +612,13 @@ public NearQuery withReadPreference(ReadPreference readPreference) { * Get the {@link ReadConcern} to use. Will return the underlying {@link #query(Query) queries} * {@link Query#getReadConcern() ReadConcern} if present or the one defined on the {@link NearQuery#readConcern} * itself. - * + * * @return can be {@literal null} if none set. * @since 4.1 * @see ReadConcernAware */ - @Nullable @Override - public ReadConcern getReadConcern() { + public @Nullable ReadConcern getReadConcern() { if (query != null && query.hasReadConcern()) { return query.getReadConcern(); @@ -620,9 +635,8 @@ public ReadConcern getReadConcern() { * @since 4.1 * @see ReadPreferenceAware */ - @Nullable @Override - public ReadPreference getReadPreference() { + public @Nullable ReadPreference getReadPreference() { if (query != null && query.hasReadPreference()) { return query.getReadPreference(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java index 31c6b9069f..47ce615fe3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java @@ -15,8 +15,9 @@ */ package org.springframework.data.mongodb.core.query; -import static org.springframework.data.mongodb.core.query.SerializationUtils.*; -import static org.springframework.util.ObjectUtils.*; +import static org.springframework.data.mongodb.core.query.SerializationUtils.serializeToJsonSafely; +import static org.springframework.util.ObjectUtils.nullSafeEquals; +import static org.springframework.util.ObjectUtils.nullSafeHashCode; import java.time.Duration; import java.util.ArrayList; @@ -30,6 +31,7 @@ import java.util.Set; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.Limit; import org.springframework.data.domain.OffsetScrollPosition; @@ -42,7 +44,7 @@ import org.springframework.data.mongodb.core.ReadPreferenceAware; import org.springframework.data.mongodb.core.query.Meta.CursorOption; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import com.mongodb.ReadConcern; @@ -69,7 +71,7 @@ public class Query implements ReadConcernAware, ReadPreferenceAware { private long skip; private Limit limit = Limit.unlimited(); - private KeysetScrollPosition keysetScrollPosition; + private @Nullable KeysetScrollPosition keysetScrollPosition; private @Nullable ReadConcern readConcern; private @Nullable ReadPreference readPreference; @@ -123,6 +125,7 @@ public Query(CriteriaDefinition criteriaDefinition) { * @return this. * @since 1.6 */ + @Contract("_ -> this") public Query addCriteria(CriteriaDefinition criteriaDefinition) { Assert.notNull(criteriaDefinition, "CriteriaDefinition must not be null"); @@ -157,6 +160,7 @@ public Field fields() { * @param skip number of documents to skip. Use {@literal zero} or a {@literal negative} value to avoid skipping. * @return this. */ + @Contract("_ -> this") public Query skip(long skip) { this.skip = skip; return this; @@ -169,6 +173,7 @@ public Query skip(long skip) { * @param limit number of documents to return. Use {@literal zero} or {@literal negative} for unlimited. * @return this. */ + @Contract("_ -> this") public Query limit(int limit) { this.limit = limit > 0 ? Limit.of(limit) : Limit.unlimited(); return this; @@ -181,6 +186,7 @@ public Query limit(int limit) { * @return this. * @since 4.2 */ + @Contract("_ -> this") public Query limit(Limit limit) { Assert.notNull(limit, "Limit must not be null"); @@ -202,6 +208,7 @@ public Query limit(Limit limit) { * @return this. * @see Document#parse(String) */ + @Contract("_ -> this") public Query withHint(String hint) { Assert.hasText(hint, "Hint must not be empty or null"); @@ -216,6 +223,7 @@ public Query withHint(String hint) { * @return this. * @since 3.1 */ + @Contract("_ -> this") public Query withReadConcern(ReadConcern readConcern) { Assert.notNull(readConcern, "ReadConcern must not be null"); @@ -230,6 +238,7 @@ public Query withReadConcern(ReadConcern readConcern) { * @return this. * @since 4.1 */ + @Contract("_ -> this") public Query withReadPreference(ReadPreference readPreference) { Assert.notNull(readPreference, "ReadPreference must not be null"); @@ -243,7 +252,7 @@ public boolean hasReadConcern() { } @Override - public ReadConcern getReadConcern() { + public @Nullable ReadConcern getReadConcern() { return this.readConcern; } @@ -253,7 +262,7 @@ public boolean hasReadPreference() { } @Override - public ReadPreference getReadPreference() { + public @Nullable ReadPreference getReadPreference() { if (readPreference == null) { return getMeta().getFlags().contains(CursorOption.SECONDARY_READS) ? ReadPreference.primaryPreferred() : null; @@ -269,6 +278,7 @@ public ReadPreference getReadPreference() { * @return this. * @since 2.2 */ + @Contract("_ -> this") public Query withHint(Document hint) { Assert.notNull(hint, "Hint must not be null"); @@ -283,6 +293,7 @@ public Query withHint(Document hint) { * @param pageable must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Query with(Pageable pageable) { if (pageable.isPaged()) { @@ -299,6 +310,7 @@ public Query with(Pageable pageable) { * @param position must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Query with(ScrollPosition position) { Assert.notNull(position, "ScrollPosition must not be null"); @@ -320,6 +332,7 @@ public Query with(ScrollPosition position) { * @param position must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Query with(OffsetScrollPosition position) { Assert.notNull(position, "ScrollPosition must not be null"); @@ -335,6 +348,7 @@ public Query with(OffsetScrollPosition position) { * @param position must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Query with(KeysetScrollPosition position) { Assert.notNull(position, "ScrollPosition must not be null"); @@ -349,8 +363,7 @@ public boolean hasKeyset() { return keysetScrollPosition != null; } - @Nullable - public KeysetScrollPosition getKeyset() { + public @Nullable KeysetScrollPosition getKeyset() { return keysetScrollPosition; } @@ -360,6 +373,7 @@ public KeysetScrollPosition getKeyset() { * @param sort must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Query with(Sort sort) { Assert.notNull(sort, "Sort must not be null"); @@ -393,6 +407,7 @@ public Set> getRestrictedTypes() { * @param additionalTypes may not be {@literal null} * @return this. */ + @Contract("_, _ -> this") public Query restrict(Class type, Class... additionalTypes) { Assert.notNull(type, "Type must not be null"); @@ -518,6 +533,7 @@ public String getHint() { * @see Meta#setMaxTimeMsec(long) * @since 1.6 */ + @Contract("_ -> this") public Query maxTimeMsec(long maxTimeMsec) { meta.setMaxTimeMsec(maxTimeMsec); @@ -530,6 +546,7 @@ public Query maxTimeMsec(long maxTimeMsec) { * @see Meta#setMaxTime(Duration) * @since 2.1 */ + @Contract("_ -> this") public Query maxTime(Duration timeout) { meta.setMaxTime(timeout); @@ -544,6 +561,7 @@ public Query maxTime(Duration timeout) { * @see Meta#setComment(String) * @since 1.6 */ + @Contract("_ -> this") public Query comment(String comment) { meta.setComment(comment); @@ -562,6 +580,7 @@ public Query comment(String comment) { * @see Meta#setAllowDiskUse(Boolean) * @since 3.2 */ + @Contract("_ -> this") public Query allowDiskUse(boolean allowDiskUse) { meta.setAllowDiskUse(allowDiskUse); @@ -578,6 +597,7 @@ public Query allowDiskUse(boolean allowDiskUse) { * @see Meta#setCursorBatchSize(int) * @since 2.1 */ + @Contract("_ -> this") public Query cursorBatchSize(int batchSize) { meta.setCursorBatchSize(batchSize); @@ -589,6 +609,7 @@ public Query cursorBatchSize(int batchSize) { * @see org.springframework.data.mongodb.core.query.Meta.CursorOption#NO_TIMEOUT * @since 1.10 */ + @Contract("-> this") public Query noCursorTimeout() { meta.addFlag(Meta.CursorOption.NO_TIMEOUT); @@ -600,6 +621,7 @@ public Query noCursorTimeout() { * @see org.springframework.data.mongodb.core.query.Meta.CursorOption#EXHAUST * @since 1.10 */ + @Contract("-> this") public Query exhaust() { meta.addFlag(Meta.CursorOption.EXHAUST); @@ -613,6 +635,7 @@ public Query exhaust() { * @see org.springframework.data.mongodb.core.query.Meta.CursorOption#SECONDARY_READS * @since 3.0.2 */ + @Contract("-> this") public Query allowSecondaryReads() { meta.addFlag(Meta.CursorOption.SECONDARY_READS); @@ -624,6 +647,7 @@ public Query allowSecondaryReads() { * @see org.springframework.data.mongodb.core.query.Meta.CursorOption#PARTIAL * @since 1.10 */ + @Contract("-> this") public Query partialResults() { meta.addFlag(Meta.CursorOption.PARTIAL); @@ -655,6 +679,7 @@ public void setMeta(Meta meta) { * @return this. * @since 2.0 */ + @Contract("_ -> this") public Query collation(@Nullable Collation collation) { this.collation = Optional.ofNullable(collation); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java index 11e0f7fb24..29f8adb2c6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java @@ -23,9 +23,9 @@ import java.util.Map; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.ObjectUtils; /** @@ -110,8 +110,8 @@ private static void toFlatMap(String currentPath, Object source, Map null; !null -> !null") + public static @Nullable String serializeToJsonSafely(@Nullable Object value) { if (value == null) { return null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Term.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Term.java index bd6d8c3469..cc87434178 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Term.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Term.java @@ -15,7 +15,8 @@ */ package org.springframework.data.mongodb.core.query; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.ObjectUtils; /** @@ -61,6 +62,7 @@ public Term(String raw, @Nullable Type type) { * * @return */ + @Contract("-> this") public Term negate() { this.negated = true; return this; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextCriteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextCriteria.java index e1a7d0c4d0..5cedc2e476 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextCriteria.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextCriteria.java @@ -19,7 +19,8 @@ import java.util.List; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -71,7 +72,8 @@ public static TextCriteria forDefaultLanguage() { * @param language * @return */ - public static TextCriteria forLanguage(String language) { + @Contract("null -> fail") + public static TextCriteria forLanguage(@Nullable String language) { Assert.hasText(language, "Language must not be null or empty"); return new TextCriteria(language); @@ -83,6 +85,7 @@ public static TextCriteria forLanguage(String language) { * @param words the words to match. * @return */ + @Contract("_ -> this") public TextCriteria matchingAny(String... words) { for (String word : words) { @@ -97,6 +100,7 @@ public TextCriteria matchingAny(String... words) { * * @param term must not be {@literal null}. */ + @Contract("_ -> this") public TextCriteria matching(Term term) { Assert.notNull(term, "Term to add must not be null"); @@ -109,6 +113,7 @@ public TextCriteria matching(Term term) { * @param term * @return */ + @Contract("_ -> this") public TextCriteria matching(String term) { if (StringUtils.hasText(term)) { @@ -121,6 +126,7 @@ public TextCriteria matching(String term) { * @param term * @return */ + @Contract("_ -> this") public TextCriteria notMatching(String term) { if (StringUtils.hasText(term)) { @@ -133,6 +139,7 @@ public TextCriteria notMatching(String term) { * @param words * @return */ + @Contract("_ -> this") public TextCriteria notMatchingAny(String... words) { for (String word : words) { @@ -147,6 +154,7 @@ public TextCriteria notMatchingAny(String... words) { * @param phrase * @return */ + @Contract("_ -> this") public TextCriteria notMatchingPhrase(String phrase) { if (StringUtils.hasText(phrase)) { @@ -161,6 +169,7 @@ public TextCriteria notMatchingPhrase(String phrase) { * @param phrase * @return */ + @Contract("_ -> this") public TextCriteria matchingPhrase(String phrase) { if (StringUtils.hasText(phrase)) { @@ -176,6 +185,7 @@ public TextCriteria matchingPhrase(String phrase) { * @return never {@literal null}. * @since 1.10 */ + @Contract("_ -> this") public TextCriteria caseSensitive(boolean caseSensitive) { this.caseSensitive = caseSensitive; @@ -189,6 +199,7 @@ public TextCriteria caseSensitive(boolean caseSensitive) { * @return never {@literal null}. * @since 1.10 */ + @Contract("_ -> this") public TextCriteria diacriticSensitive(boolean diacriticSensitive) { this.diacriticSensitive = diacriticSensitive; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextQuery.java index a6583299d6..a9f82a857f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextQuery.java @@ -19,8 +19,9 @@ import java.util.Map.Entry; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; /** * {@link Query} implementation to be used to for performing full text searches. @@ -100,6 +101,7 @@ public static TextQuery queryText(TextCriteria criteria) { * @see TextQuery#includeScore() * @return this. */ + @Contract("-> this") public TextQuery sortByScore() { this.sortByScoreIndex = getSortObject().size(); @@ -113,6 +115,7 @@ public TextQuery sortByScore() { * * @return this. */ + @Contract("-> this") public TextQuery includeScore() { this.includeScore = true; @@ -125,6 +128,7 @@ public TextQuery includeScore() { * @param fieldname must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public TextQuery includeScore(String fieldname) { setScoreFieldName(fieldname); @@ -170,9 +174,8 @@ public Document getSortObject() { int sortByScoreIndex = this.sortByScoreIndex; - return sortByScoreIndex != 0 - ? sortByScoreAtPosition(super.getSortObject(), sortByScoreIndex) - : sortByScoreAtPositionZero(); + return sortByScoreIndex != 0 ? sortByScoreAtPosition(super.getSortObject(), sortByScoreIndex) + : sortByScoreAtPositionZero(); } return super.getSortObject(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/UntypedExampleMatcher.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/UntypedExampleMatcher.java index 677575c9e4..c02425214d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/UntypedExampleMatcher.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/UntypedExampleMatcher.java @@ -17,8 +17,8 @@ import java.util.Set; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.ExampleMatcher; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java index 32d98f5804..cfb214a5a3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java @@ -27,11 +27,12 @@ import java.util.Set; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -114,6 +115,7 @@ public static Update fromDocument(Document object, String... exclude) { * @return this. * @see MongoDB Update operator: $set */ + @Contract("_, _ -> this") public Update set(String key, @Nullable Object value) { addMultiFieldOperation("$set", key, value); return this; @@ -128,6 +130,7 @@ public Update set(String key, @Nullable Object value) { * @see MongoDB Update operator: * $setOnInsert */ + @Contract("_, _ -> this") public Update setOnInsert(String key, @Nullable Object value) { addMultiFieldOperation("$setOnInsert", key, value); return this; @@ -140,6 +143,7 @@ public Update setOnInsert(String key, @Nullable Object value) { * @return this. * @see MongoDB Update operator: $unset */ + @Contract("_ -> this") public Update unset(String key) { addMultiFieldOperation("$unset", key, 1); return this; @@ -153,12 +157,14 @@ public Update unset(String key) { * @return this. * @see MongoDB Update operator: $inc */ + @Contract("_, _ -> this") public Update inc(String key, Number inc) { addMultiFieldOperation("$inc", key, inc); return this; } @Override + @Contract("_ -> this") public void inc(String key) { inc(key, 1L); } @@ -171,6 +177,7 @@ public void inc(String key) { * @return this. * @see MongoDB Update operator: $push */ + @Contract("_, _ -> this") public Update push(String key, @Nullable Object value) { addMultiFieldOperation("$push", key, value); return this; @@ -207,6 +214,7 @@ public PushOperatorBuilder push(String key) { * @return new instance of {@link AddToSetBuilder}. * @since 1.5 */ + @Contract("_ -> new") public AddToSetBuilder addToSet(String key) { return new AddToSetBuilder(key); } @@ -220,6 +228,7 @@ public AddToSetBuilder addToSet(String key) { * @see MongoDB Update operator: * $addToSet */ + @Contract("_, _ -> this") public Update addToSet(String key, @Nullable Object value) { addMultiFieldOperation("$addToSet", key, value); return this; @@ -233,6 +242,7 @@ public Update addToSet(String key, @Nullable Object value) { * @return this. * @see MongoDB Update operator: $pop */ + @Contract("_, _ -> this") public Update pop(String key, Position pos) { addMultiFieldOperation("$pop", key, pos == Position.FIRST ? -1 : 1); return this; @@ -246,6 +256,7 @@ public Update pop(String key, Position pos) { * @return this. * @see MongoDB Update operator: $pull */ + @Contract("_, _ -> this") public Update pull(String key, @Nullable Object value) { addMultiFieldOperation("$pull", key, value); return this; @@ -260,6 +271,7 @@ public Update pull(String key, @Nullable Object value) { * @see MongoDB Update operator: * $pullAll */ + @Contract("_, _ -> this") public Update pullAll(String key, Object[] values) { addMultiFieldOperation("$pullAll", key, Arrays.asList(values)); return this; @@ -274,6 +286,7 @@ public Update pullAll(String key, Object[] values) { * @see MongoDB Update operator: * $rename */ + @Contract("_, _ -> this") public Update rename(String oldName, String newName) { addMultiFieldOperation("$rename", oldName, newName); return this; @@ -288,6 +301,7 @@ public Update rename(String oldName, String newName) { * @see MongoDB Update operator: * $currentDate */ + @Contract("_ -> this") public Update currentDate(String key) { addMultiFieldOperation("$currentDate", key, true); @@ -303,6 +317,7 @@ public Update currentDate(String key) { * @see MongoDB Update operator: * $currentDate */ + @Contract("_ -> this") public Update currentTimestamp(String key) { addMultiFieldOperation("$currentDate", key, new Document("$type", "timestamp")); @@ -318,6 +333,7 @@ public Update currentTimestamp(String key) { * @since 1.7 * @see MongoDB Update operator: $mul */ + @Contract("_, _ -> this") public Update multiply(String key, Number multiplier) { Assert.notNull(multiplier, "Multiplier must not be null"); @@ -335,6 +351,7 @@ public Update multiply(String key, Number multiplier) { * @see Comparison/Sort Order * @see MongoDB Update operator: $max */ + @Contract("_, _ -> this") public Update max(String key, Object value) { Assert.notNull(value, "Value for max operation must not be null"); @@ -352,6 +369,7 @@ public Update max(String key, Object value) { * @see Comparison/Sort Order * @see MongoDB Update operator: $min */ + @Contract("_, _ -> this") public Update min(String key, Object value) { Assert.notNull(value, "Value for min operation must not be null"); @@ -366,6 +384,7 @@ public Update min(String key, Object value) { * @return this. * @since 1.7 */ + @Contract("_ -> new") public BitwiseOperatorBuilder bitwise(String key) { return new BitwiseOperatorBuilder(this, key); } @@ -378,6 +397,7 @@ public BitwiseOperatorBuilder bitwise(String key) { * @return this. * @since 2.0 */ + @Contract("-> this") public Update isolated() { isolated = true; @@ -392,6 +412,7 @@ public Update isolated() { * @return this. * @since 2.2 */ + @Contract("_ -> this") public Update filterArray(CriteriaDefinition criteria) { if (arrayFilters == Collections.EMPTY_LIST) { @@ -411,6 +432,7 @@ public Update filterArray(CriteriaDefinition criteria) { * @return this. * @since 2.2 */ + @Contract("_, _ -> this") public Update filterArray(String identifier, Object expression) { if (arrayFilters == Collections.EMPTY_LIST) { @@ -815,6 +837,7 @@ public Update each(Object... values) { * @return never {@literal null}. * @since 1.10 */ + @Contract("_ -> this") public PushOperatorBuilder slice(int count) { this.modifiers.addModifier(new Slice(count)); @@ -829,6 +852,7 @@ public PushOperatorBuilder slice(int count) { * @return never {@literal null}. * @since 1.10 */ + @Contract("_ -> this") public PushOperatorBuilder sort(Direction direction) { Assert.notNull(direction, "Direction must not be null"); @@ -844,6 +868,7 @@ public PushOperatorBuilder sort(Direction direction) { * @return never {@literal null}. * @since 1.10 */ + @Contract("_ -> this") public PushOperatorBuilder sort(Sort sort) { Assert.notNull(sort, "Sort must not be null"); @@ -859,6 +884,7 @@ public PushOperatorBuilder sort(Sort sort) { * @return never {@literal null}. * @since 1.7 */ + @Contract("_ -> this") public PushOperatorBuilder atPosition(int position) { this.modifiers.addModifier(new PositionModifier(position)); @@ -872,6 +898,7 @@ public PushOperatorBuilder atPosition(int position) { * @return never {@literal null}. * @since 1.7 */ + @Contract("_ -> this") public PushOperatorBuilder atPosition(@Nullable Position position) { if (position == null || Position.LAST.equals(position)) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/package-info.java index d3f67790a1..7c6889e45b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/package-info.java @@ -1,6 +1,6 @@ /** * MongoDB specific query and update support. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.query; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/DefaultMongoJsonSchema.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/DefaultMongoJsonSchema.java index b59c20c6b6..da77a0199f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/DefaultMongoJsonSchema.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/DefaultMongoJsonSchema.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core.schema; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -31,8 +31,7 @@ class DefaultMongoJsonSchema implements MongoJsonSchema { private final JsonSchemaObject root; - @Nullable // - private final Document encryptionMetadata; + private final @Nullable Document encryptionMetadata; DefaultMongoJsonSchema(JsonSchemaObject root) { this(root, null); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java index 29cedfd6ce..503d591d99 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java @@ -23,8 +23,9 @@ import java.util.UUID; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Range; +import org.springframework.data.mongodb.core.EncryptionAlgorithms; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ArrayJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.BooleanJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.DateJsonSchemaObject; @@ -33,7 +34,7 @@ import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.StringJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.TimestampJsonSchemaObject; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -97,6 +98,7 @@ public static class UntypedJsonSchemaProperty extends IdentifiableJsonSchemaProp * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> new") public UntypedJsonSchemaProperty possibleValues(Object... possibleValues) { return possibleValues(Arrays.asList(possibleValues)); } @@ -106,6 +108,7 @@ public UntypedJsonSchemaProperty possibleValues(Object... possibleValues) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> new") public UntypedJsonSchemaProperty allOf(JsonSchemaObject... allOf) { return allOf(new LinkedHashSet<>(Arrays.asList(allOf))); } @@ -115,6 +118,7 @@ public UntypedJsonSchemaProperty allOf(JsonSchemaObject... allOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> new") public UntypedJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) { return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf))); } @@ -124,6 +128,7 @@ public UntypedJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> new") public UntypedJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) { return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf))); } @@ -133,6 +138,7 @@ public UntypedJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> new") public UntypedJsonSchemaProperty possibleValues(Collection possibleValues) { return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues)); } @@ -142,6 +148,7 @@ public UntypedJsonSchemaProperty possibleValues(Collection possibleValue * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> new") public UntypedJsonSchemaProperty allOf(Collection allOf) { return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf)); } @@ -151,6 +158,7 @@ public UntypedJsonSchemaProperty allOf(Collection allOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> new") public UntypedJsonSchemaProperty anyOf(Collection anyOf) { return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf)); } @@ -160,6 +168,7 @@ public UntypedJsonSchemaProperty anyOf(Collection anyOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> new") public UntypedJsonSchemaProperty oneOf(Collection oneOf) { return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf)); } @@ -169,6 +178,7 @@ public UntypedJsonSchemaProperty oneOf(Collection oneOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#notMatch(JsonSchemaObject) */ + @Contract("_ -> new") public UntypedJsonSchemaProperty notMatch(JsonSchemaObject notMatch) { return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch)); } @@ -178,6 +188,7 @@ public UntypedJsonSchemaProperty notMatch(JsonSchemaObject notMatch) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#description(String) */ + @Contract("_ -> new") public UntypedJsonSchemaProperty description(String description) { return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); } @@ -186,6 +197,7 @@ public UntypedJsonSchemaProperty description(String description) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#generateDescription() */ + @Contract("_ -> new") public UntypedJsonSchemaProperty generatedDescription() { return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription()); } @@ -213,6 +225,7 @@ public static class StringJsonSchemaProperty extends IdentifiableJsonSchemaPrope * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#minLength(int) */ + @Contract("_ -> new") public StringJsonSchemaProperty minLength(int length) { return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.minLength(length)); } @@ -222,6 +235,7 @@ public StringJsonSchemaProperty minLength(int length) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#maxLength(int) */ + @Contract("_ -> new") public StringJsonSchemaProperty maxLength(int length) { return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxLength(length)); } @@ -231,6 +245,7 @@ public StringJsonSchemaProperty maxLength(int length) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#matching(String) */ + @Contract("_ -> new") public StringJsonSchemaProperty matching(String pattern) { return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.matching(pattern)); } @@ -240,6 +255,7 @@ public StringJsonSchemaProperty matching(String pattern) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> new") public StringJsonSchemaProperty possibleValues(String... possibleValues) { return possibleValues(Arrays.asList(possibleValues)); } @@ -249,6 +265,7 @@ public StringJsonSchemaProperty possibleValues(String... possibleValues) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> new") public StringJsonSchemaProperty allOf(JsonSchemaObject... allOf) { return allOf(new LinkedHashSet<>(Arrays.asList(allOf))); } @@ -258,6 +275,7 @@ public StringJsonSchemaProperty allOf(JsonSchemaObject... allOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> new") public StringJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) { return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf))); } @@ -267,6 +285,7 @@ public StringJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> new") public StringJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) { return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf))); } @@ -276,6 +295,7 @@ public StringJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> new") public StringJsonSchemaProperty possibleValues(Collection possibleValues) { return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues)); } @@ -285,6 +305,7 @@ public StringJsonSchemaProperty possibleValues(Collection possibleValues * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> new") public StringJsonSchemaProperty allOf(Collection allOf) { return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf)); } @@ -294,6 +315,7 @@ public StringJsonSchemaProperty allOf(Collection allOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> new") public StringJsonSchemaProperty anyOf(Collection anyOf) { return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf)); } @@ -303,6 +325,7 @@ public StringJsonSchemaProperty anyOf(Collection anyOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> new") public StringJsonSchemaProperty oneOf(Collection oneOf) { return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf)); } @@ -312,6 +335,7 @@ public StringJsonSchemaProperty oneOf(Collection oneOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#notMatch(JsonSchemaObject) */ + @Contract("_ -> new") public StringJsonSchemaProperty notMatch(JsonSchemaObject notMatch) { return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch)); } @@ -321,6 +345,7 @@ public StringJsonSchemaProperty notMatch(JsonSchemaObject notMatch) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#description(String) */ + @Contract("_ -> new") public StringJsonSchemaProperty description(String description) { return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); } @@ -329,6 +354,7 @@ public StringJsonSchemaProperty description(String description) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#generateDescription() */ + @Contract("_ -> new") public StringJsonSchemaProperty generatedDescription() { return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription()); } @@ -355,6 +381,7 @@ public static class ObjectJsonSchemaProperty extends IdentifiableJsonSchemaPrope * @param range must not be {@literal null}. * @return new instance of {@link ObjectJsonSchemaProperty}. */ + @Contract("_ -> new") public ObjectJsonSchemaProperty propertiesCount(Range range) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.propertiesCount(range)); } @@ -364,6 +391,7 @@ public ObjectJsonSchemaProperty propertiesCount(Range range) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#minProperties(int) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty minProperties(int count) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.minProperties(count)); } @@ -373,6 +401,7 @@ public ObjectJsonSchemaProperty minProperties(int count) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#maxProperties(int) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty maxProperties(int count) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxProperties(count)); } @@ -382,6 +411,7 @@ public ObjectJsonSchemaProperty maxProperties(int count) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#required(String...) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty required(String... properties) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.required(properties)); } @@ -391,6 +421,7 @@ public ObjectJsonSchemaProperty required(String... properties) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#additionalProperties(boolean) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty additionalProperties(boolean additionalPropertiesAllowed) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.additionalProperties(additionalPropertiesAllowed)); @@ -401,6 +432,7 @@ public ObjectJsonSchemaProperty additionalProperties(boolean additionalPropertie * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#additionalProperties(ObjectJsonSchemaObject) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty additionalProperties(ObjectJsonSchemaObject additionalProperties) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.additionalProperties(additionalProperties)); @@ -411,6 +443,7 @@ public ObjectJsonSchemaProperty additionalProperties(ObjectJsonSchemaObject addi * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#properties(JsonSchemaProperty...) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty properties(JsonSchemaProperty... properties) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.properties(properties)); } @@ -420,6 +453,7 @@ public ObjectJsonSchemaProperty properties(JsonSchemaProperty... properties) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty possibleValues(Object... possibleValues) { return possibleValues(Arrays.asList(possibleValues)); } @@ -429,6 +463,7 @@ public ObjectJsonSchemaProperty possibleValues(Object... possibleValues) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty allOf(JsonSchemaObject... allOf) { return allOf(new LinkedHashSet<>(Arrays.asList(allOf))); } @@ -438,6 +473,7 @@ public ObjectJsonSchemaProperty allOf(JsonSchemaObject... allOf) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) { return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf))); } @@ -447,6 +483,7 @@ public ObjectJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) { return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf))); } @@ -456,6 +493,7 @@ public ObjectJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty possibleValues(Collection possibleValues) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues)); } @@ -465,6 +503,7 @@ public ObjectJsonSchemaProperty possibleValues(Collection possibleValues * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty allOf(Collection allOf) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf)); } @@ -474,6 +513,7 @@ public ObjectJsonSchemaProperty allOf(Collection allOf) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty anyOf(Collection anyOf) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf)); } @@ -483,6 +523,7 @@ public ObjectJsonSchemaProperty anyOf(Collection anyOf) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty oneOf(Collection oneOf) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf)); } @@ -492,6 +533,7 @@ public ObjectJsonSchemaProperty oneOf(Collection oneOf) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#notMatch(JsonSchemaObject) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty notMatch(JsonSchemaObject notMatch) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch)); } @@ -501,6 +543,7 @@ public ObjectJsonSchemaProperty notMatch(JsonSchemaObject notMatch) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#description(String) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty description(String description) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); } @@ -509,6 +552,7 @@ public ObjectJsonSchemaProperty description(String description) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#generateDescription() */ + @Contract("_ -> new") public ObjectJsonSchemaProperty generatedDescription() { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription()); } @@ -540,6 +584,7 @@ public NumericJsonSchemaProperty(String identifier, NumericJsonSchemaObject sche * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#multipleOf */ + @Contract("_ -> new") public NumericJsonSchemaProperty multipleOf(Number value) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.multipleOf(value)); } @@ -549,6 +594,7 @@ public NumericJsonSchemaProperty multipleOf(Number value) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#within(Range) */ + @Contract("_ -> new") public NumericJsonSchemaProperty within(Range range) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.within(range)); } @@ -558,6 +604,7 @@ public NumericJsonSchemaProperty within(Range range) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#gt(Number) */ + @Contract("_ -> new") public NumericJsonSchemaProperty gt(Number min) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.gt(min)); } @@ -567,6 +614,7 @@ public NumericJsonSchemaProperty gt(Number min) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#gte(Number) */ + @Contract("_ -> new") public NumericJsonSchemaProperty gte(Number min) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.gte(min)); } @@ -576,6 +624,7 @@ public NumericJsonSchemaProperty gte(Number min) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#lt(Number) */ + @Contract("_ -> new") public NumericJsonSchemaProperty lt(Number max) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.lt(max)); } @@ -585,6 +634,7 @@ public NumericJsonSchemaProperty lt(Number max) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#lte(Number) */ + @Contract("_ -> new") public NumericJsonSchemaProperty lte(Number max) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.lte(max)); } @@ -594,6 +644,7 @@ public NumericJsonSchemaProperty lte(Number max) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> new") public NumericJsonSchemaProperty possibleValues(Number... possibleValues) { return possibleValues(new LinkedHashSet<>(Arrays.asList(possibleValues))); } @@ -603,6 +654,7 @@ public NumericJsonSchemaProperty possibleValues(Number... possibleValues) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> new") public NumericJsonSchemaProperty allOf(JsonSchemaObject... allOf) { return allOf(Arrays.asList(allOf)); } @@ -612,6 +664,7 @@ public NumericJsonSchemaProperty allOf(JsonSchemaObject... allOf) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> new") public NumericJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) { return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf))); } @@ -621,6 +674,7 @@ public NumericJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> new") public NumericJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) { return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf))); } @@ -630,6 +684,7 @@ public NumericJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> new") public NumericJsonSchemaProperty possibleValues(Collection possibleValues) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues)); } @@ -639,6 +694,7 @@ public NumericJsonSchemaProperty possibleValues(Collection possibleValue * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> new") public NumericJsonSchemaProperty allOf(Collection allOf) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf)); } @@ -648,6 +704,7 @@ public NumericJsonSchemaProperty allOf(Collection allOf) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> new") public NumericJsonSchemaProperty anyOf(Collection anyOf) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf)); } @@ -657,6 +714,7 @@ public NumericJsonSchemaProperty anyOf(Collection anyOf) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> new") public NumericJsonSchemaProperty oneOf(Collection oneOf) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf)); } @@ -666,6 +724,7 @@ public NumericJsonSchemaProperty oneOf(Collection oneOf) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#notMatch(JsonSchemaObject) */ + @Contract("_ -> new") public NumericJsonSchemaProperty notMatch(JsonSchemaObject notMatch) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch)); } @@ -675,6 +734,7 @@ public NumericJsonSchemaProperty notMatch(JsonSchemaObject notMatch) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#description(String) */ + @Contract("_ -> new") public NumericJsonSchemaProperty description(String description) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); } @@ -710,6 +770,7 @@ public ArrayJsonSchemaProperty(String identifier, ArrayJsonSchemaObject schemaOb * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#uniqueItems(boolean) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty uniqueItems(boolean uniqueItems) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.uniqueItems(uniqueItems)); } @@ -719,6 +780,7 @@ public ArrayJsonSchemaProperty uniqueItems(boolean uniqueItems) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#range(Range) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty range(Range range) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.range(range)); } @@ -728,6 +790,7 @@ public ArrayJsonSchemaProperty range(Range range) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#minItems(int) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty minItems(int count) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.minItems(count)); } @@ -737,6 +800,7 @@ public ArrayJsonSchemaProperty minItems(int count) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#maxItems(int) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty maxItems(int count) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxItems(count)); } @@ -746,6 +810,7 @@ public ArrayJsonSchemaProperty maxItems(int count) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#items(Collection) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty items(JsonSchemaObject... items) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.items(Arrays.asList(items))); } @@ -755,6 +820,7 @@ public ArrayJsonSchemaProperty items(JsonSchemaObject... items) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#items(Collection) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty items(Collection items) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.items(items)); } @@ -764,6 +830,7 @@ public ArrayJsonSchemaProperty items(Collection items) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#additionalItems(boolean) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty additionalItems(boolean additionalItemsAllowed) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.additionalItems(additionalItemsAllowed)); } @@ -773,6 +840,7 @@ public ArrayJsonSchemaProperty additionalItems(boolean additionalItemsAllowed) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty possibleValues(Object... possibleValues) { return possibleValues(new LinkedHashSet<>(Arrays.asList(possibleValues))); } @@ -782,6 +850,7 @@ public ArrayJsonSchemaProperty possibleValues(Object... possibleValues) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty allOf(JsonSchemaObject... allOf) { return allOf(new LinkedHashSet<>(Arrays.asList(allOf))); } @@ -791,6 +860,7 @@ public ArrayJsonSchemaProperty allOf(JsonSchemaObject... allOf) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) { return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf))); } @@ -800,6 +870,7 @@ public ArrayJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) { return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf))); } @@ -809,6 +880,7 @@ public ArrayJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty possibleValues(Collection possibleValues) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues)); } @@ -818,6 +890,7 @@ public ArrayJsonSchemaProperty possibleValues(Collection possibleValues) * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty allOf(Collection allOf) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf)); } @@ -827,6 +900,7 @@ public ArrayJsonSchemaProperty allOf(Collection allOf) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty anyOf(Collection anyOf) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf)); } @@ -836,6 +910,7 @@ public ArrayJsonSchemaProperty anyOf(Collection anyOf) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty oneOf(Collection oneOf) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf)); } @@ -845,6 +920,7 @@ public ArrayJsonSchemaProperty oneOf(Collection oneOf) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#notMatch(JsonSchemaObject) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty notMatch(JsonSchemaObject notMatch) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch)); } @@ -854,6 +930,7 @@ public ArrayJsonSchemaProperty notMatch(JsonSchemaObject notMatch) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see ArrayJsonSchemaObject#description(String) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty description(String description) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); } @@ -884,6 +961,7 @@ public static class BooleanJsonSchemaProperty extends IdentifiableJsonSchemaProp * @return new instance of {@link NumericJsonSchemaProperty}. * @see BooleanJsonSchemaObject#description(String) */ + @Contract("_ -> new") public BooleanJsonSchemaProperty description(String description) { return new BooleanJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); } @@ -914,6 +992,7 @@ public static class NullJsonSchemaProperty extends IdentifiableJsonSchemaPropert * @return new instance of {@link NullJsonSchemaProperty}. * @see NullJsonSchemaObject#description(String) */ + @Contract("_ -> new") public NullJsonSchemaProperty description(String description) { return new NullJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); } @@ -944,6 +1023,7 @@ public static class DateJsonSchemaProperty extends IdentifiableJsonSchemaPropert * @return new instance of {@link DateJsonSchemaProperty}. * @see DateJsonSchemaProperty#description(String) */ + @Contract("_ -> new") public DateJsonSchemaProperty description(String description) { return new DateJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); } @@ -974,6 +1054,7 @@ public static class TimestampJsonSchemaProperty extends IdentifiableJsonSchemaPr * @return new instance of {@link TimestampJsonSchemaProperty}. * @see TimestampJsonSchemaProperty#description(String) */ + @Contract("_ -> new") public TimestampJsonSchemaProperty description(String description) { return new TimestampJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); } @@ -1036,7 +1117,7 @@ public static class EncryptedJsonSchemaProperty implements JsonSchemaProperty { private final JsonSchemaProperty targetProperty; private final @Nullable String algorithm; - private final @Nullable String keyId; + private final @Nullable Object keyId; private final @Nullable List keyIds; /** @@ -1048,7 +1129,7 @@ public EncryptedJsonSchemaProperty(JsonSchemaProperty target) { this(target, null, null, null); } - private EncryptedJsonSchemaProperty(JsonSchemaProperty target, @Nullable String algorithm, @Nullable String keyId, + private EncryptedJsonSchemaProperty(JsonSchemaProperty target, @Nullable String algorithm, @Nullable Object keyId, @Nullable List keyIds) { Assert.notNull(target, "Target must not be null"); @@ -1068,13 +1149,26 @@ public static EncryptedJsonSchemaProperty encrypted(JsonSchemaProperty target) { return new EncryptedJsonSchemaProperty(target); } + /** + * Create new instance of {@link EncryptedJsonSchemaProperty} with {@literal Range} encryption, wrapping the given + * {@link JsonSchemaProperty target}. + * + * @param target must not be {@literal null}. + * @return new instance of {@link EncryptedJsonSchemaProperty}. + * @since 4.5 + */ + public static EncryptedJsonSchemaProperty rangeEncrypted(JsonSchemaProperty target) { + return new EncryptedJsonSchemaProperty(target).algorithm(EncryptionAlgorithms.RANGE); + } + /** * Use {@literal AEAD_AES_256_CBC_HMAC_SHA_512-Random} algorithm. * * @return new instance of {@link EncryptedJsonSchemaProperty}. */ + @Contract("-> new") public EncryptedJsonSchemaProperty aead_aes_256_cbc_hmac_sha_512_random() { - return algorithm("AEAD_AES_256_CBC_HMAC_SHA_512-Random"); + return algorithm(EncryptionAlgorithms.AEAD_AES_256_CBC_HMAC_SHA_512_Random); } /** @@ -1082,8 +1176,9 @@ public EncryptedJsonSchemaProperty aead_aes_256_cbc_hmac_sha_512_random() { * * @return new instance of {@link EncryptedJsonSchemaProperty}. */ + @Contract("-> new") public EncryptedJsonSchemaProperty aead_aes_256_cbc_hmac_sha_512_deterministic() { - return algorithm("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"); + return algorithm(EncryptionAlgorithms.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); } /** @@ -1091,6 +1186,7 @@ public EncryptedJsonSchemaProperty aead_aes_256_cbc_hmac_sha_512_deterministic() * * @return new instance of {@link EncryptedJsonSchemaProperty}. */ + @Contract("_ -> new") public EncryptedJsonSchemaProperty algorithm(String algorithm) { return new EncryptedJsonSchemaProperty(targetProperty, algorithm, keyId, keyIds); } @@ -1099,14 +1195,25 @@ public EncryptedJsonSchemaProperty algorithm(String algorithm) { * @param keyId must not be {@literal null}. * @return new instance of {@link EncryptedJsonSchemaProperty}. */ + @Contract("_ -> new") public EncryptedJsonSchemaProperty keyId(String keyId) { return new EncryptedJsonSchemaProperty(targetProperty, algorithm, keyId, null); } + /** + * @param keyId must not be {@literal null}. + * @return new instance of {@link EncryptedJsonSchemaProperty}. + * @since 4.5 + */ + public EncryptedJsonSchemaProperty keyId(Object keyId) { + return new EncryptedJsonSchemaProperty(targetProperty, algorithm, keyId, null); + } + /** * @param keyId must not be {@literal null}. * @return new instance of {@link EncryptedJsonSchemaProperty}. */ + @Contract("_ -> new") public EncryptedJsonSchemaProperty keys(UUID... keyId) { return new EncryptedJsonSchemaProperty(targetProperty, algorithm, null, Arrays.asList(keyId)); } @@ -1115,6 +1222,7 @@ public EncryptedJsonSchemaProperty keys(UUID... keyId) { * @param keyId must not be {@literal null}. * @return new instance of {@link EncryptedJsonSchemaProperty}. */ + @Contract("_ -> new") public EncryptedJsonSchemaProperty keys(Object... keyId) { return new EncryptedJsonSchemaProperty(targetProperty, algorithm, null, Arrays.asList(keyId)); } @@ -1159,8 +1267,8 @@ public Set getTypes() { return targetProperty.getTypes(); } - @Nullable - private Type extractPropertyType(Document source) { + + private @Nullable Type extractPropertyType(Document source) { if (source.containsKey("type")) { return Type.of(source.get("type", String.class)); @@ -1171,5 +1279,71 @@ private Type extractPropertyType(Document source) { return null; } + + public @Nullable Object getKeyId() { + if (keyId != null) { + return keyId; + } + if (keyIds != null && keyIds.size() == 1) { + return keyIds.iterator().next(); + } + return null; + } + } + + /** + * {@link JsonSchemaProperty} implementation typically wrapping an {@link EncryptedJsonSchemaProperty encrypted + * property} to mark it as queryable. + * + * @author Christoph Strobl + * @since 4.5 + */ + public static class QueryableJsonSchemaProperty implements JsonSchemaProperty { + + private final JsonSchemaProperty targetProperty; + private final QueryCharacteristics characteristics; + + public QueryableJsonSchemaProperty(JsonSchemaProperty target, QueryCharacteristics characteristics) { + this.targetProperty = target; + this.characteristics = characteristics; + } + + @Override + public Document toDocument() { + + Document doc = targetProperty.toDocument(); + Document propertySpecification = doc.get(targetProperty.getIdentifier(), Document.class); + + if (propertySpecification.containsKey("encrypt")) { + Document encrypt = propertySpecification.get("encrypt", Document.class); + List queries = characteristics.getCharacteristics().stream().map(QueryCharacteristic::toDocument) + .toList(); + encrypt.append("queries", queries); + } + + return doc; + } + + @Override + public String getIdentifier() { + return targetProperty.getIdentifier(); + } + + @Override + public Set getTypes() { + return targetProperty.getTypes(); + } + + boolean isEncrypted() { + return targetProperty instanceof EncryptedJsonSchemaProperty; + } + + public JsonSchemaProperty getTargetProperty() { + return targetProperty; + } + + public QueryCharacteristics getCharacteristics() { + return characteristics; + } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaObject.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaObject.java index a84f361d37..24a40efa5b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaObject.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaObject.java @@ -31,6 +31,7 @@ import org.bson.types.Code; import org.bson.types.Decimal128; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ArrayJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.BooleanJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.DateJsonSchemaObject; @@ -39,7 +40,6 @@ import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.StringJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.TimestampJsonSchemaObject; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java index 8529951db2..20d735ee03 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java @@ -16,11 +16,23 @@ package org.springframework.data.mongodb.core.schema; import java.util.Collection; +import java.util.List; +import org.jspecify.annotations.Nullable; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ArrayJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.BooleanJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.DateJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.NullJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.NumericJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ObjectJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.QueryableJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.RequiredJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.StringJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.TimestampJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.UntypedJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.NumericJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject; -import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.*; -import org.springframework.lang.Nullable; /** * A {@literal property} or {@literal patternProperty} within a {@link JsonSchemaObject} of {@code type : 'object'}. @@ -69,6 +81,18 @@ static EncryptedJsonSchemaProperty encrypted(JsonSchemaProperty property) { return EncryptedJsonSchemaProperty.encrypted(property); } + /** + * Turns the given target property into a {@link QueryableJsonSchemaProperty queryable} one, eg. for {@literal range} + * encrypted properties. + * + * @param property the queryable property. Must not be {@literal null}. + * @param queries predefined query characteristics. + * @since 4.5 + */ + static QueryableJsonSchemaProperty queryable(JsonSchemaProperty property, List queries) { + return new QueryableJsonSchemaProperty(property, new QueryCharacteristics(queries)); + } + /** * Creates a new {@link StringJsonSchemaProperty} with given {@literal identifier} of {@code type : 'string'}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MergedJsonSchema.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MergedJsonSchema.java index e0f3e26100..a6fc3ab8bd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MergedJsonSchema.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MergedJsonSchema.java @@ -19,7 +19,9 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.BiFunction; +import java.util.stream.Collectors; import org.bson.Document; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MongoJsonSchema.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MongoJsonSchema.java index f64218cc56..87c46d63dc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MongoJsonSchema.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MongoJsonSchema.java @@ -23,8 +23,9 @@ import java.util.Set; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -212,7 +213,7 @@ interface Path { /** * @return the name of the currently processed element */ - String currentElement(); + @Nullable String currentElement(); /** * @return the path leading to the currently processed element in dot {@literal '.'} notation. @@ -285,11 +286,11 @@ static Resolution ofValue(Path path, Object value) { * @param value the value to apply. * @return */ - static Resolution ofValue(String key, Object value) { + static Resolution ofValue(@Nullable String key, Object value) { return new Resolution() { @Override - public String getKey() { + public @Nullable String getKey() { return key; } @@ -311,8 +312,7 @@ class MongoJsonSchemaBuilder { private ObjectJsonSchemaObject root; - @Nullable // - private Document encryptionMetadata; + private @Nullable Document encryptionMetadata; MongoJsonSchemaBuilder() { root = new ObjectJsonSchemaObject(); @@ -323,6 +323,7 @@ class MongoJsonSchemaBuilder { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#minProperties(int) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder minProperties(int count) { root = root.minProperties(count); @@ -334,6 +335,7 @@ public MongoJsonSchemaBuilder minProperties(int count) { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#maxProperties(int) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder maxProperties(int count) { root = root.maxProperties(count); @@ -345,6 +347,7 @@ public MongoJsonSchemaBuilder maxProperties(int count) { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#required(String...) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder required(String... properties) { root = root.required(properties); @@ -356,6 +359,7 @@ public MongoJsonSchemaBuilder required(String... properties) { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#additionalProperties(boolean) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder additionalProperties(boolean additionalPropertiesAllowed) { root = root.additionalProperties(additionalPropertiesAllowed); @@ -367,6 +371,7 @@ public MongoJsonSchemaBuilder additionalProperties(boolean additionalPropertiesA * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#additionalProperties(ObjectJsonSchemaObject) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder additionalProperties(ObjectJsonSchemaObject schema) { root = root.additionalProperties(schema); @@ -378,6 +383,7 @@ public MongoJsonSchemaBuilder additionalProperties(ObjectJsonSchemaObject schema * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#properties(JsonSchemaProperty...) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder properties(JsonSchemaProperty... properties) { root = root.properties(properties); @@ -389,6 +395,7 @@ public MongoJsonSchemaBuilder properties(JsonSchemaProperty... properties) { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#patternProperties(JsonSchemaProperty...) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder patternProperties(JsonSchemaProperty... properties) { root = root.patternProperties(properties); @@ -400,6 +407,7 @@ public MongoJsonSchemaBuilder patternProperties(JsonSchemaProperty... properties * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#property(JsonSchemaProperty) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder property(JsonSchemaProperty property) { root = root.property(property); @@ -411,6 +419,7 @@ public MongoJsonSchemaBuilder property(JsonSchemaProperty property) { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder possibleValues(Set possibleValues) { root = root.possibleValues(possibleValues); @@ -422,6 +431,7 @@ public MongoJsonSchemaBuilder possibleValues(Set possibleValues) { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see UntypedJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder allOf(Set allOf) { root = root.allOf(allOf); @@ -433,6 +443,7 @@ public MongoJsonSchemaBuilder allOf(Set allOf) { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see UntypedJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder anyOf(Set anyOf) { root = root.anyOf(anyOf); @@ -444,6 +455,7 @@ public MongoJsonSchemaBuilder anyOf(Set anyOf) { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see UntypedJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder oneOf(Set oneOf) { root = root.oneOf(oneOf); @@ -455,6 +467,7 @@ public MongoJsonSchemaBuilder oneOf(Set oneOf) { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see UntypedJsonSchemaObject#notMatch(JsonSchemaObject) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder notMatch(JsonSchemaObject notMatch) { root = root.notMatch(notMatch); @@ -466,6 +479,7 @@ public MongoJsonSchemaBuilder notMatch(JsonSchemaObject notMatch) { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see UntypedJsonSchemaObject#description(String) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder description(String description) { root = root.description(description); @@ -487,6 +501,7 @@ public void encryptionMetadata(@Nullable Document encryptionMetadata) { * * @return new instance of {@link MongoJsonSchema}. */ + @Contract("-> new") public MongoJsonSchema build() { return new DefaultMongoJsonSchema(root, encryptionMetadata); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristic.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristic.java new file mode 100644 index 0000000000..8604ba9d6c --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristic.java @@ -0,0 +1,40 @@ +/* + * 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.mongodb.core.schema; + +import org.bson.Document; + +/** + * Defines the specific character of a query that can be executed. Mainly used to define the characteristic of queryable + * encrypted fields. + * + * @author Christoph Strobl + * @since 4.5 + */ +public interface QueryCharacteristic { + + /** + * @return the query type, eg. {@literal range}. + */ + String queryType(); + + /** + * @return the raw {@link Document} representation of the instance. + */ + default Document toDocument() { + return new Document("queryType", queryType()); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java new file mode 100644 index 0000000000..9283bf4afa --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java @@ -0,0 +1,263 @@ +/* + * 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.mongodb.core.schema; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.bson.BsonNull; +import org.bson.Document; +import org.jspecify.annotations.Nullable; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Range.Bound; + +/** + * Encapsulation of individual {@link QueryCharacteristic query characteristics} used to define queries that can be + * executed when using queryable encryption. + * + * @author Christoph Strobl + * @since 4.5 + */ +public class QueryCharacteristics implements Iterable { + + /** + * instance indicating none + */ + private static final QueryCharacteristics NONE = new QueryCharacteristics(Collections.emptyList()); + + private final List characteristics; + + QueryCharacteristics(List characteristics) { + this.characteristics = characteristics; + } + + /** + * @return marker instance indicating no characteristics have been defined. + */ + public static QueryCharacteristics none() { + return NONE; + } + + /** + * Create new {@link QueryCharacteristics} from given list of {@link QueryCharacteristic characteristics}. + * + * @param characteristics must not be {@literal null}. + * @return new instance of {@link QueryCharacteristics}. + */ + public static QueryCharacteristics of(List characteristics) { + return new QueryCharacteristics(List.copyOf(characteristics)); + } + + /** + * Create new {@link QueryCharacteristics} from given {@link QueryCharacteristic characteristics}. + * + * @param characteristics must not be {@literal null}. + * @return new instance of {@link QueryCharacteristics}. + */ + public static QueryCharacteristics of(QueryCharacteristic... characteristics) { + return new QueryCharacteristics(Arrays.asList(characteristics)); + } + + /** + * @return the list of {@link QueryCharacteristic characteristics}. + */ + public List getCharacteristics() { + return characteristics; + } + + @Override + public Iterator iterator() { + return this.characteristics.iterator(); + } + + /** + * Create a new {@link RangeQuery range query characteristic} used to define range queries against an encrypted field. + * + * @param targeted field type + * @return new instance of {@link RangeQuery}. + */ + public static RangeQuery range() { + return new RangeQuery<>(); + } + + /** + * Create a new {@link EqualityQuery equality query characteristic} used to define equality queries against an + * encrypted field. + * + * @param targeted field type + * @return new instance of {@link EqualityQuery}. + */ + public static EqualityQuery equality() { + return new EqualityQuery<>(null); + } + + /** + * {@link QueryCharacteristic} for equality comparison. + * + * @param + * @since 4.5 + */ + public static class EqualityQuery implements QueryCharacteristic { + + private final @Nullable Long contention; + + /** + * Create new instance of {@link EqualityQuery}. + * + * @param contention can be {@literal null}. + */ + public EqualityQuery(@Nullable Long contention) { + this.contention = contention; + } + + /** + * @param contention concurrent counter partition factor. + * @return new instance of {@link EqualityQuery}. + */ + public EqualityQuery contention(long contention) { + return new EqualityQuery<>(contention); + } + + @Override + public String queryType() { + return "equality"; + } + + @Override + public Document toDocument() { + return QueryCharacteristic.super.toDocument().append("contention", contention); + } + } + + /** + * {@link QueryCharacteristic} for range comparison. + * + * @param + * @since 4.5 + */ + public static class RangeQuery implements QueryCharacteristic { + + private final @Nullable Range valueRange; + private final @Nullable Integer trimFactor; + private final @Nullable Long sparsity; + private final @Nullable Long precision; + private final @Nullable Long contention; + + private RangeQuery() { + this(Range.unbounded(), null, null, null, null); + } + + /** + * Create new instance of {@link RangeQuery}. + * + * @param valueRange + * @param trimFactor + * @param sparsity + * @param contention + */ + public RangeQuery(@Nullable Range valueRange, @Nullable Integer trimFactor, @Nullable Long sparsity, + @Nullable Long precision, @Nullable Long contention) { + this.valueRange = valueRange; + this.trimFactor = trimFactor; + this.sparsity = sparsity; + this.precision = precision; + this.contention = contention; + } + + /** + * @param lower the lower value range boundary for the queryable field. + * @return new instance of {@link RangeQuery}. + */ + public RangeQuery min(T lower) { + + Range range = Range.of(Bound.inclusive(lower), + valueRange != null ? valueRange.getUpperBound() : Bound.unbounded()); + return new RangeQuery<>(range, trimFactor, sparsity, precision, contention); + } + + /** + * @param upper the upper value range boundary for the queryable field. + * @return new instance of {@link RangeQuery}. + */ + public RangeQuery max(T upper) { + + Range range = Range.of(valueRange != null ? valueRange.getLowerBound() : Bound.unbounded(), + Bound.inclusive(upper)); + return new RangeQuery<>(range, trimFactor, sparsity, precision, contention); + } + + /** + * @param trimFactor value to control the throughput of concurrent inserts and updates. + * @return new instance of {@link RangeQuery}. + */ + public RangeQuery trimFactor(int trimFactor) { + return new RangeQuery<>(valueRange, trimFactor, sparsity, precision, contention); + } + + /** + * @param sparsity value to control the value density within the index. + * @return new instance of {@link RangeQuery}. + */ + public RangeQuery sparsity(long sparsity) { + return new RangeQuery<>(valueRange, trimFactor, sparsity, precision, contention); + } + + /** + * @param contention concurrent counter partition factor. + * @return new instance of {@link RangeQuery}. + */ + public RangeQuery contention(long contention) { + return new RangeQuery<>(valueRange, trimFactor, sparsity, precision, contention); + } + + /** + * @param precision digits considered comparing floating point numbers. + * @return new instance of {@link RangeQuery}. + */ + public RangeQuery precision(long precision) { + return new RangeQuery<>(valueRange, trimFactor, sparsity, precision, contention); + } + + @Override + public String queryType() { + return "range"; + } + + @Override + @SuppressWarnings("unchecked") + public Document toDocument() { + + Document target = QueryCharacteristic.super.toDocument(); + if (contention != null) { + target.append("contention", contention); + } + if (trimFactor != null) { + target.append("trimFactor", trimFactor); + } + if (valueRange != null) { + target.append("min", valueRange.getLowerBound().getValue().orElse((T) BsonNull.VALUE)).append("max", + valueRange.getUpperBound().getValue().orElse((T) BsonNull.VALUE)); + } + if (sparsity != null) { + target.append("sparsity", sparsity); + } + + return target; + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypeUnifyingMergeFunction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypeUnifyingMergeFunction.java index 95f116619f..87bdd8c618 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypeUnifyingMergeFunction.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypeUnifyingMergeFunction.java @@ -22,10 +22,10 @@ import java.util.function.BiFunction; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.schema.MongoJsonSchema.ConflictResolutionFunction; import org.springframework.data.mongodb.core.schema.MongoJsonSchema.ConflictResolutionFunction.Path; import org.springframework.data.mongodb.core.schema.MongoJsonSchema.ConflictResolutionFunction.Resolution; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -119,8 +119,7 @@ private static String getTypeKeyToUse(String key, Document source) { return key; } - @Nullable - private static Object getUnifiedExistingType(String key, Document source) { + private static @Nullable Object getUnifiedExistingType(String key, Document source) { return source.get(getTypeKeyToUse(key, source)); } @@ -155,7 +154,7 @@ public SimplePath append(String next) { } @Override - public String currentElement() { + public @Nullable String currentElement() { return CollectionUtils.lastElement(path); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypedJsonSchemaObject.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypedJsonSchemaObject.java index abf8b0b8a2..7b299fd4d2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypedJsonSchemaObject.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypedJsonSchemaObject.java @@ -27,9 +27,10 @@ import java.util.stream.Collectors; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; @@ -100,6 +101,7 @@ public Set getTypes() { * @return new instance of {@link TypedJsonSchemaObject}. */ @Override + @Contract("_ -> new") public TypedJsonSchemaObject description(String description) { return new TypedJsonSchemaObject(types, description, generateDescription, restrictions); } @@ -110,6 +112,7 @@ public TypedJsonSchemaObject description(String description) { * @return new instance of {@link TypedJsonSchemaObject}. */ @Override + @Contract("-> new") public TypedJsonSchemaObject generatedDescription() { return new TypedJsonSchemaObject(types, description, true, restrictions); } @@ -121,6 +124,7 @@ public TypedJsonSchemaObject generatedDescription() { * @return new instance of {@link TypedJsonSchemaObject}. */ @Override + @Contract("_ -> new") public TypedJsonSchemaObject possibleValues(Collection possibleValues) { return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.possibleValues(possibleValues)); @@ -133,6 +137,7 @@ public TypedJsonSchemaObject possibleValues(Collection possibl * @return new instance of {@link TypedJsonSchemaObject}. */ @Override + @Contract("_ -> new") public TypedJsonSchemaObject allOf(Collection allOf) { return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.allOf(allOf)); } @@ -144,6 +149,7 @@ public TypedJsonSchemaObject allOf(Collection allOf) { * @return new instance of {@link TypedJsonSchemaObject}. */ @Override + @Contract("_ -> new") public TypedJsonSchemaObject anyOf(Collection anyOf) { return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.anyOf(anyOf)); } @@ -155,6 +161,7 @@ public TypedJsonSchemaObject anyOf(Collection anyOf) { * @return new instance of {@link TypedJsonSchemaObject}. */ @Override + @Contract("_ -> new") public TypedJsonSchemaObject oneOf(Collection oneOf) { return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.oneOf(oneOf)); } @@ -166,6 +173,7 @@ public TypedJsonSchemaObject oneOf(Collection oneOf) { * @return new instance of {@link TypedJsonSchemaObject}. */ @Override + @Contract("_ -> new") public TypedJsonSchemaObject notMatch(JsonSchemaObject notMatch) { return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.notMatch(notMatch)); } @@ -210,8 +218,7 @@ private Optional getOrCreateDescription() { * * @return can be {@literal null}. */ - @Nullable - protected String generateDescription() { + protected @Nullable String generateDescription() { return null; } @@ -264,6 +271,7 @@ public ObjectJsonSchemaObject propertiesCount(Range range) { * @param count the allowed minimal number of properties. * @return new instance of {@link ObjectJsonSchemaObject}. */ + @Contract("_ -> new") public ObjectJsonSchemaObject minProperties(int count) { Bound upper = this.propertiesCount != null ? this.propertiesCount.getUpperBound() : Bound.unbounded(); @@ -276,6 +284,7 @@ public ObjectJsonSchemaObject minProperties(int count) { * @param count the allowed maximum number of properties. * @return new instance of {@link ObjectJsonSchemaObject}. */ + @Contract("_ -> new") public ObjectJsonSchemaObject maxProperties(int count) { Bound lower = this.propertiesCount != null ? this.propertiesCount.getLowerBound() : Bound.unbounded(); @@ -288,6 +297,7 @@ public ObjectJsonSchemaObject maxProperties(int count) { * @param properties the names of required properties. * @return new instance of {@link ObjectJsonSchemaObject}. */ + @Contract("_ -> new") public ObjectJsonSchemaObject required(String... properties) { ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); @@ -305,6 +315,7 @@ public ObjectJsonSchemaObject required(String... properties) { * @param additionalPropertiesAllowed * @return new instance of {@link ObjectJsonSchemaObject}. */ + @Contract("_ -> new") public ObjectJsonSchemaObject additionalProperties(boolean additionalPropertiesAllowed) { ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); @@ -319,6 +330,7 @@ public ObjectJsonSchemaObject additionalProperties(boolean additionalPropertiesA * @param schema must not be {@literal null}. * @return new instance of {@link ObjectJsonSchemaObject}. */ + @Contract("_ -> new") public ObjectJsonSchemaObject additionalProperties(ObjectJsonSchemaObject schema) { ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); @@ -332,6 +344,7 @@ public ObjectJsonSchemaObject additionalProperties(ObjectJsonSchemaObject schema * @param properties must not be {@literal null}. * @return new instance of {@link ObjectJsonSchemaObject}. */ + @Contract("_ -> new") public ObjectJsonSchemaObject properties(JsonSchemaProperty... properties) { ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); @@ -349,6 +362,7 @@ public ObjectJsonSchemaObject properties(JsonSchemaProperty... properties) { * @param regularExpressions must not be {@literal null}. * @return new instance of {@link ObjectJsonSchemaObject}. */ + @Contract("_ -> new") public ObjectJsonSchemaObject patternProperties(JsonSchemaProperty... regularExpressions) { ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); @@ -365,41 +379,49 @@ public ObjectJsonSchemaObject patternProperties(JsonSchemaProperty... regularExp * @param property must not be {@literal null}. * @return new instance of {@link ObjectJsonSchemaObject}. */ + @Contract("_ -> new") public ObjectJsonSchemaObject property(JsonSchemaProperty property) { return properties(property); } @Override + @Contract("_ -> new") public ObjectJsonSchemaObject possibleValues(Collection possibleValues) { return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues)); } @Override + @Contract("_ -> new") public ObjectJsonSchemaObject allOf(Collection allOf) { return newInstance(description, generateDescription, restrictions.allOf(allOf)); } @Override + @Contract("_ -> new") public ObjectJsonSchemaObject anyOf(Collection anyOf) { return newInstance(description, generateDescription, restrictions.anyOf(anyOf)); } @Override + @Contract("_ -> new") public ObjectJsonSchemaObject oneOf(Collection oneOf) { return newInstance(description, generateDescription, restrictions.oneOf(oneOf)); } @Override + @Contract("_ -> new") public ObjectJsonSchemaObject notMatch(JsonSchemaObject notMatch) { return newInstance(description, generateDescription, restrictions.notMatch(notMatch)); } @Override + @Contract("_ -> new") public ObjectJsonSchemaObject description(String description) { return newInstance(description, generateDescription, restrictions); } @Override + @Contract("_ -> new") public ObjectJsonSchemaObject generatedDescription() { return newInstance(description, true, restrictions); } @@ -545,6 +567,7 @@ private NumericJsonSchemaObject(Set types, @Nullable String description, b * @param value must not be {@literal null}. * @return must not be {@literal null}. */ + @Contract("_ -> new") public NumericJsonSchemaObject multipleOf(Number value) { Assert.notNull(value, "Value must not be null"); @@ -561,6 +584,7 @@ public NumericJsonSchemaObject multipleOf(Number value) { * @param range must not be {@literal null}. * @return new instance of {@link NumericJsonSchemaObject}. */ + @Contract("_ -> new") public NumericJsonSchemaObject within(Range range) { Assert.notNull(range, "Range must not be null"); @@ -578,6 +602,7 @@ public NumericJsonSchemaObject within(Range range) { * @return new instance of {@link NumericJsonSchemaObject}. */ @SuppressWarnings("unchecked") + @Contract("_ -> new") public NumericJsonSchemaObject gt(Number min) { Assert.notNull(min, "Min must not be null"); @@ -593,6 +618,7 @@ public NumericJsonSchemaObject gt(Number min) { * @return new instance of {@link NumericJsonSchemaObject}. */ @SuppressWarnings("unchecked") + @Contract("_ -> new") public NumericJsonSchemaObject gte(Number min) { Assert.notNull(min, "Min must not be null"); @@ -608,6 +634,7 @@ public NumericJsonSchemaObject gte(Number min) { * @return new instance of {@link NumericJsonSchemaObject}. */ @SuppressWarnings("unchecked") + @Contract("_ -> new") public NumericJsonSchemaObject lt(Number max) { Assert.notNull(max, "Max must not be null"); @@ -623,6 +650,7 @@ public NumericJsonSchemaObject lt(Number max) { * @return new instance of {@link NumericJsonSchemaObject}. */ @SuppressWarnings("unchecked") + @Contract("_ -> new") public NumericJsonSchemaObject lte(Number max) { Assert.notNull(max, "Max must not be null"); @@ -632,36 +660,43 @@ public NumericJsonSchemaObject lte(Number max) { } @Override + @Contract("_ -> new") public NumericJsonSchemaObject possibleValues(Collection possibleValues) { return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues)); } @Override + @Contract("_ -> new") public NumericJsonSchemaObject allOf(Collection allOf) { return newInstance(description, generateDescription, restrictions.allOf(allOf)); } @Override + @Contract("_ -> new") public NumericJsonSchemaObject anyOf(Collection anyOf) { return newInstance(description, generateDescription, restrictions.anyOf(anyOf)); } @Override + @Contract("_ -> new") public NumericJsonSchemaObject oneOf(Collection oneOf) { return newInstance(description, generateDescription, restrictions.oneOf(oneOf)); } @Override + @Contract("_ -> new") public NumericJsonSchemaObject notMatch(JsonSchemaObject notMatch) { return newInstance(description, generateDescription, restrictions.notMatch(notMatch)); } @Override + @Contract("_ -> new") public NumericJsonSchemaObject description(String description) { return newInstance(description, generateDescription, restrictions); } @Override + @Contract("_ -> new") public NumericJsonSchemaObject generatedDescription() { return newInstance(description, true, restrictions); } @@ -785,6 +820,7 @@ private StringJsonSchemaObject(@Nullable String description, boolean generateDes * @param range must not be {@literal null}. * @return new instance of {@link StringJsonSchemaObject}. */ + @Contract("_ -> new") public StringJsonSchemaObject length(Range range) { Assert.notNull(range, "Range must not be null"); @@ -801,6 +837,7 @@ public StringJsonSchemaObject length(Range range) { * @param length * @return new instance of {@link StringJsonSchemaObject}. */ + @Contract("_ -> new") public StringJsonSchemaObject minLength(int length) { Bound upper = this.length != null ? this.length.getUpperBound() : Bound.unbounded(); @@ -813,6 +850,7 @@ public StringJsonSchemaObject minLength(int length) { * @param length * @return new instance of {@link StringJsonSchemaObject}. */ + @Contract("_ -> new") public StringJsonSchemaObject maxLength(int length) { Bound lower = this.length != null ? this.length.getLowerBound() : Bound.unbounded(); @@ -825,6 +863,7 @@ public StringJsonSchemaObject maxLength(int length) { * @param pattern must not be {@literal null}. * @return new instance of {@link StringJsonSchemaObject}. */ + @Contract("_ -> new") public StringJsonSchemaObject matching(String pattern) { Assert.notNull(pattern, "Pattern must not be null"); @@ -836,36 +875,43 @@ public StringJsonSchemaObject matching(String pattern) { } @Override + @Contract("_ -> new") public StringJsonSchemaObject possibleValues(Collection possibleValues) { return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues)); } @Override + @Contract("_ -> new") public StringJsonSchemaObject allOf(Collection allOf) { return newInstance(description, generateDescription, restrictions.allOf(allOf)); } @Override + @Contract("_ -> new") public StringJsonSchemaObject anyOf(Collection anyOf) { return newInstance(description, generateDescription, restrictions.anyOf(anyOf)); } @Override + @Contract("_ -> new") public StringJsonSchemaObject oneOf(Collection oneOf) { return newInstance(description, generateDescription, restrictions.oneOf(oneOf)); } @Override + @Contract("_ -> new") public StringJsonSchemaObject notMatch(JsonSchemaObject notMatch) { return newInstance(description, generateDescription, restrictions.notMatch(notMatch)); } @Override + @Contract("_ -> new") public StringJsonSchemaObject description(String description) { return newInstance(description, generateDescription, restrictions); } @Override + @Contract("-> new") public StringJsonSchemaObject generatedDescription() { return newInstance(description, true, restrictions); } @@ -946,6 +992,7 @@ private ArrayJsonSchemaObject(@Nullable String description, boolean generateDesc * @param uniqueItems * @return new instance of {@link ArrayJsonSchemaObject}. */ + @Contract("_ -> new") public ArrayJsonSchemaObject uniqueItems(boolean uniqueItems) { ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); @@ -961,6 +1008,7 @@ public ArrayJsonSchemaObject uniqueItems(boolean uniqueItems) { * @param range must not be {@literal null}. Consider {@link Range#unbounded()} instead. * @return new instance of {@link ArrayJsonSchemaObject}. */ + @Contract("_ -> new") public ArrayJsonSchemaObject range(Range range) { ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); @@ -975,6 +1023,7 @@ public ArrayJsonSchemaObject range(Range range) { * @param count the allowed minimal number of array items. * @return new instance of {@link ArrayJsonSchemaObject}. */ + @Contract("_ -> new") public ArrayJsonSchemaObject minItems(int count) { Bound upper = this.range != null ? this.range.getUpperBound() : Bound.unbounded(); @@ -987,6 +1036,7 @@ public ArrayJsonSchemaObject minItems(int count) { * @param count the allowed maximal number of array items. * @return new instance of {@link ArrayJsonSchemaObject}. */ + @Contract("_ -> new") public ArrayJsonSchemaObject maxItems(int count) { Bound lower = this.range != null ? this.range.getLowerBound() : Bound.unbounded(); @@ -999,6 +1049,7 @@ public ArrayJsonSchemaObject maxItems(int count) { * @param items the allowed items in the array. * @return new instance of {@link ArrayJsonSchemaObject}. */ + @Contract("_ -> new") public ArrayJsonSchemaObject items(Collection items) { ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); @@ -1013,6 +1064,7 @@ public ArrayJsonSchemaObject items(Collection items) { * @param additionalItemsAllowed {@literal true} to allow additional items in the array, {@literal false} otherwise. * @return new instance of {@link ArrayJsonSchemaObject}. */ + @Contract("_ -> new") public ArrayJsonSchemaObject additionalItems(boolean additionalItemsAllowed) { ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); @@ -1022,36 +1074,43 @@ public ArrayJsonSchemaObject additionalItems(boolean additionalItemsAllowed) { } @Override + @Contract("_ -> new") public ArrayJsonSchemaObject possibleValues(Collection possibleValues) { return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues)); } @Override + @Contract("_ -> new") public ArrayJsonSchemaObject allOf(Collection allOf) { return newInstance(description, generateDescription, restrictions.allOf(allOf)); } @Override + @Contract("_ -> new") public ArrayJsonSchemaObject anyOf(Collection anyOf) { return newInstance(description, generateDescription, restrictions.anyOf(anyOf)); } @Override + @Contract("_ -> new") public ArrayJsonSchemaObject oneOf(Collection oneOf) { return newInstance(description, generateDescription, restrictions.oneOf(oneOf)); } @Override + @Contract("_ -> new") public ArrayJsonSchemaObject notMatch(JsonSchemaObject notMatch) { return newInstance(description, generateDescription, restrictions.notMatch(notMatch)); } @Override + @Contract("_ -> new") public ArrayJsonSchemaObject description(String description) { return newInstance(description, generateDescription, restrictions); } @Override + @Contract("_ -> new") public ArrayJsonSchemaObject generatedDescription() { return newInstance(description, true, restrictions); } @@ -1147,41 +1206,49 @@ private BooleanJsonSchemaObject(@Nullable String description, boolean generateDe } @Override + @Contract("_ -> new") public BooleanJsonSchemaObject possibleValues(Collection possibleValues) { return new BooleanJsonSchemaObject(description, generateDescription, restrictions.possibleValues(possibleValues)); } @Override + @Contract("_ -> new") public BooleanJsonSchemaObject allOf(Collection allOf) { return new BooleanJsonSchemaObject(description, generateDescription, restrictions.allOf(allOf)); } @Override + @Contract("_ -> new") public BooleanJsonSchemaObject anyOf(Collection anyOf) { return new BooleanJsonSchemaObject(description, generateDescription, restrictions.anyOf(anyOf)); } @Override + @Contract("_ -> new") public BooleanJsonSchemaObject oneOf(Collection oneOf) { return new BooleanJsonSchemaObject(description, generateDescription, restrictions.oneOf(oneOf)); } @Override + @Contract("_ -> new") public BooleanJsonSchemaObject notMatch(JsonSchemaObject notMatch) { return new BooleanJsonSchemaObject(description, generateDescription, restrictions.notMatch(notMatch)); } @Override + @Contract("_ -> new") public BooleanJsonSchemaObject description(String description) { return new BooleanJsonSchemaObject(description, generateDescription, restrictions); } @Override + @Contract("_ -> new") public BooleanJsonSchemaObject generatedDescription() { return new BooleanJsonSchemaObject(description, true, restrictions); } @Override + @Contract("-> new") protected String generateDescription() { return "Must be a boolean"; } @@ -1208,36 +1275,43 @@ private NullJsonSchemaObject(@Nullable String description, boolean generateDescr } @Override + @Contract("_ -> new") public NullJsonSchemaObject possibleValues(Collection possibleValues) { return new NullJsonSchemaObject(description, generateDescription, restrictions.possibleValues(possibleValues)); } @Override + @Contract("_ -> new") public NullJsonSchemaObject allOf(Collection allOf) { return new NullJsonSchemaObject(description, generateDescription, restrictions.allOf(allOf)); } @Override + @Contract("_ -> new") public NullJsonSchemaObject anyOf(Collection anyOf) { return new NullJsonSchemaObject(description, generateDescription, restrictions.anyOf(anyOf)); } @Override + @Contract("_ -> new") public NullJsonSchemaObject oneOf(Collection oneOf) { return new NullJsonSchemaObject(description, generateDescription, restrictions.oneOf(oneOf)); } @Override + @Contract("_ -> new") public NullJsonSchemaObject notMatch(JsonSchemaObject notMatch) { return new NullJsonSchemaObject(description, generateDescription, restrictions.notMatch(notMatch)); } @Override + @Contract("_ -> new") public NullJsonSchemaObject description(String description) { return new NullJsonSchemaObject(description, generateDescription, restrictions); } @Override + @Contract("-> new") public NullJsonSchemaObject generatedDescription() { return new NullJsonSchemaObject(description, true, restrictions); } @@ -1268,36 +1342,43 @@ private DateJsonSchemaObject(@Nullable String description, boolean generateDescr } @Override + @Contract("_ -> new") public DateJsonSchemaObject possibleValues(Collection possibleValues) { return new DateJsonSchemaObject(description, generateDescription, restrictions.possibleValues(possibleValues)); } @Override + @Contract("_ -> new") public DateJsonSchemaObject allOf(Collection allOf) { return new DateJsonSchemaObject(description, generateDescription, restrictions.allOf(allOf)); } @Override + @Contract("_ -> new") public DateJsonSchemaObject anyOf(Collection anyOf) { return new DateJsonSchemaObject(description, generateDescription, restrictions.anyOf(anyOf)); } @Override + @Contract("_ -> new") public DateJsonSchemaObject oneOf(Collection oneOf) { return new DateJsonSchemaObject(description, generateDescription, restrictions.oneOf(oneOf)); } @Override + @Contract("_ -> new") public DateJsonSchemaObject notMatch(JsonSchemaObject notMatch) { return new DateJsonSchemaObject(description, generateDescription, restrictions.notMatch(notMatch)); } @Override + @Contract("_ -> new") public DateJsonSchemaObject description(String description) { return new DateJsonSchemaObject(description, generateDescription, restrictions); } @Override + @Contract("-> new") public DateJsonSchemaObject generatedDescription() { return new DateJsonSchemaObject(description, true, restrictions); } @@ -1328,37 +1409,44 @@ private TimestampJsonSchemaObject(@Nullable String description, boolean generate } @Override + @Contract("_ -> new") public TimestampJsonSchemaObject possibleValues(Collection possibleValues) { return new TimestampJsonSchemaObject(description, generateDescription, restrictions.possibleValues(possibleValues)); } @Override + @Contract("_ -> new") public TimestampJsonSchemaObject allOf(Collection allOf) { return new TimestampJsonSchemaObject(description, generateDescription, restrictions.allOf(allOf)); } @Override + @Contract("_ -> new") public TimestampJsonSchemaObject anyOf(Collection anyOf) { return new TimestampJsonSchemaObject(description, generateDescription, restrictions.anyOf(anyOf)); } @Override + @Contract("_ -> new") public TimestampJsonSchemaObject oneOf(Collection oneOf) { return new TimestampJsonSchemaObject(description, generateDescription, restrictions.oneOf(oneOf)); } @Override + @Contract("_ -> new") public TimestampJsonSchemaObject notMatch(JsonSchemaObject notMatch) { return new TimestampJsonSchemaObject(description, generateDescription, restrictions.notMatch(notMatch)); } @Override + @Contract("_ -> new") public TimestampJsonSchemaObject description(String description) { return new TimestampJsonSchemaObject(description, generateDescription, restrictions); } @Override + @Contract("-> new") public TimestampJsonSchemaObject generatedDescription() { return new TimestampJsonSchemaObject(description, true, restrictions); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/UntypedJsonSchemaObject.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/UntypedJsonSchemaObject.java index 54ca29e0e3..d13f8d7985 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/UntypedJsonSchemaObject.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/UntypedJsonSchemaObject.java @@ -23,7 +23,8 @@ import java.util.stream.Collectors; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -69,6 +70,7 @@ public Set getTypes() { * @param description must not be {@literal null}. * @return new instance of {@link TypedJsonSchemaObject}. */ + @Contract("_ -> new") public UntypedJsonSchemaObject description(String description) { return new UntypedJsonSchemaObject(restrictions, description, generateDescription); } @@ -78,6 +80,7 @@ public UntypedJsonSchemaObject description(String description) { * * @return new instance of {@link TypedJsonSchemaObject}. */ + @Contract("-> new") public UntypedJsonSchemaObject generatedDescription() { return new UntypedJsonSchemaObject(restrictions, description, true); } @@ -88,6 +91,7 @@ public UntypedJsonSchemaObject generatedDescription() { * @param possibleValues must not be {@literal null}. * @return new instance of {@link TypedJsonSchemaObject}. */ + @Contract("_ -> new") public UntypedJsonSchemaObject possibleValues(Collection possibleValues) { return new UntypedJsonSchemaObject(restrictions.possibleValues(possibleValues), description, generateDescription); } @@ -98,6 +102,7 @@ public UntypedJsonSchemaObject possibleValues(Collection possi * @param allOf must not be {@literal null}. * @return new instance of {@link TypedJsonSchemaObject}. */ + @Contract("_ -> new") public UntypedJsonSchemaObject allOf(Collection allOf) { return new UntypedJsonSchemaObject(restrictions.allOf(allOf), description, generateDescription); } @@ -108,6 +113,7 @@ public UntypedJsonSchemaObject allOf(Collection allOf) { * @param anyOf must not be {@literal null}. * @return new instance of {@link TypedJsonSchemaObject}. */ + @Contract("_ -> new") public UntypedJsonSchemaObject anyOf(Collection anyOf) { return new UntypedJsonSchemaObject(restrictions.anyOf(anyOf), description, generateDescription); } @@ -118,6 +124,7 @@ public UntypedJsonSchemaObject anyOf(Collection anyOf) { * @param oneOf must not be {@literal null}. * @return new instance of {@link TypedJsonSchemaObject}. */ + @Contract("_ -> new") public UntypedJsonSchemaObject oneOf(Collection oneOf) { return new UntypedJsonSchemaObject(restrictions.oneOf(oneOf), description, generateDescription); } @@ -128,6 +135,7 @@ public UntypedJsonSchemaObject oneOf(Collection oneOf) { * @param notMatch must not be {@literal null}. * @return new instance of {@link TypedJsonSchemaObject}. */ + @Contract("_ -> new") public UntypedJsonSchemaObject notMatch(JsonSchemaObject notMatch) { return new UntypedJsonSchemaObject(restrictions.notMatch(notMatch), description, generateDescription); } @@ -163,8 +171,7 @@ private Optional getOrCreateDescription() { * * @return can be {@literal null}. */ - @Nullable - protected String generateDescription() { + protected @Nullable String generateDescription() { return null; } @@ -177,14 +184,14 @@ protected String generateDescription() { */ static class Restrictions { - private final Collection possibleValues; + private final Collection possibleValues; private final Collection allOf; private final Collection anyOf; private final Collection oneOf; private final @Nullable JsonSchemaObject notMatch; - Restrictions(Collection possibleValues, Collection allOf, - Collection anyOf, Collection oneOf, JsonSchemaObject notMatch) { + Restrictions(Collection possibleValues, Collection allOf, + Collection anyOf, Collection oneOf, @Nullable JsonSchemaObject notMatch) { this.possibleValues = possibleValues; this.allOf = allOf; @@ -206,7 +213,8 @@ static Restrictions empty() { * @param possibleValues must not be {@literal null}. * @return */ - Restrictions possibleValues(Collection possibleValues) { + @Contract("_ -> new") + Restrictions possibleValues(Collection possibleValues) { Assert.notNull(possibleValues, "PossibleValues must not be null"); return new Restrictions(possibleValues, allOf, anyOf, oneOf, notMatch); @@ -216,6 +224,7 @@ Restrictions possibleValues(Collection possibleValues) { * @param allOf must not be {@literal null}. * @return */ + @Contract("_ -> new") Restrictions allOf(Collection allOf) { Assert.notNull(allOf, "AllOf must not be null"); @@ -226,6 +235,7 @@ Restrictions allOf(Collection allOf) { * @param anyOf must not be {@literal null}. * @return */ + @Contract("_ -> new") Restrictions anyOf(Collection anyOf) { Assert.notNull(anyOf, "AnyOf must not be null"); @@ -236,6 +246,7 @@ Restrictions anyOf(Collection anyOf) { * @param oneOf must not be {@literal null}. * @return */ + @Contract("_ -> new") Restrictions oneOf(Collection oneOf) { Assert.notNull(oneOf, "OneOf must not be null"); @@ -246,6 +257,7 @@ Restrictions oneOf(Collection oneOf) { * @param notMatch must not be {@literal null}. * @return */ + @Contract("_ -> new") Restrictions notMatch(JsonSchemaObject notMatch) { Assert.notNull(notMatch, "NotMatch must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/package-info.java index 380d92af09..cdc583e038 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/package-info.java @@ -1,6 +1,6 @@ /** * MongoDB-specific JSON schema implementation classes. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked @org.springframework.lang.NonNullFields package org.springframework.data.mongodb.core.schema; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/package-info.java index 34eb8ea890..976b238fbd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/package-info.java @@ -3,6 +3,6 @@ * * @since 1.7 */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.script; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionNode.java index b4550ee8de..a5b4a2aabf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionNode.java @@ -18,13 +18,13 @@ import java.util.Collections; import java.util.Iterator; +import org.jspecify.annotations.Nullable; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelNode; import org.springframework.expression.spel.ast.Literal; import org.springframework.expression.spel.ast.MethodReference; import org.springframework.expression.spel.ast.Operator; import org.springframework.expression.spel.ast.OperatorNot; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -150,8 +150,7 @@ public boolean isLiteral() { * * @return */ - @Nullable - public Object getValue() { + public @Nullable Object getValue() { return node.getValue(state); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformationContextSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformationContextSupport.java index 8869f51e09..89edd4eab2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformationContextSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformationContextSupport.java @@ -18,7 +18,7 @@ import java.util.List; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** @@ -67,8 +67,7 @@ public T getCurrentNode() { * * @return */ - @Nullable - public ExpressionNode getParentNode() { + public @Nullable ExpressionNode getParentNode() { return parentNode; } @@ -81,8 +80,7 @@ public ExpressionNode getParentNode() { * @see #addToPreviousOrReturn(Object) * @return */ - @Nullable - public Document getPreviousOperationObject() { + public @Nullable Document getPreviousOperationObject() { return previousOperationObject; } @@ -110,7 +108,7 @@ public boolean parentIsSameOperation() { * @param value * @return */ - public Document addToPreviousOperation(Object value) { + public Document addToPreviousOperation(@Nullable Object value) { Assert.state(previousOperationObject != null, "No previous operation available"); @@ -124,11 +122,14 @@ public Document addToPreviousOperation(Object value) { * @param value * @return */ - public Object addToPreviousOrReturn(Object value) { + public @Nullable Object addToPreviousOrReturn(@Nullable Object value) { return hasPreviousOperation() ? addToPreviousOperation(value) : value; } + @SuppressWarnings("unchecked") private List extractArgumentListFrom(Document context) { - return (List) context.get(context.keySet().iterator().next()); + + Object o = context.get(context.keySet().iterator().next()); + return o instanceof List l ? (List) l : List.of(); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformer.java index 512f753042..da5748f523 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformer.java @@ -15,6 +15,8 @@ */ package org.springframework.data.mongodb.core.spel; +import org.jspecify.annotations.Nullable; + /** * SPI interface to implement components that can transform an {@link ExpressionTransformationContextSupport} into an * object. @@ -29,5 +31,5 @@ public interface ExpressionTransformer new") public Options contentType(String contentType) { Options target = new Options(new Document(metadata), chunkSize); @@ -121,6 +123,7 @@ public Options contentType(String contentType) { * @param metadata * @return new instance of {@link Options}. */ + @Contract("_ -> new") public Options metadata(Document metadata) { return new Options(metadata, chunkSize); } @@ -129,6 +132,7 @@ public Options metadata(Document metadata) { * @param chunkSize the file chunk size to use. * @return new instance of {@link Options}. */ + @Contract("_ -> new") public Options chunkSize(int chunkSize) { return new Options(metadata, chunkSize); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperations.java index bf5a1d86e3..4878b431f4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperations.java @@ -19,11 +19,11 @@ import org.bson.Document; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.gridfs.GridFsUpload.GridFsUploadBuilder; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -181,8 +181,7 @@ default ObjectId store(InputStream content, @Nullable String filename, @Nullable * @param query must not be {@literal null}. * @return can be {@literal null}. */ - @Nullable - com.mongodb.client.gridfs.model.GridFSFile findOne(Query query); + com.mongodb.client.gridfs.model.@Nullable GridFSFile findOne(Query query); /** * Deletes all files matching the given {@link Query}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperationsSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperationsSupport.java index b3d3771f3c..9a5621dcbc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperationsSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperationsSupport.java @@ -18,9 +18,9 @@ import java.util.Optional; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.convert.QueryMapper; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsResource.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsResource.java index 0873432977..db6ce9833d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsResource.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsResource.java @@ -21,10 +21,10 @@ import java.io.InputStream; import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.mongodb.MongoGridFSException; @@ -105,6 +105,7 @@ public InputStream getInputStream() throws IOException, IllegalStateException { } @Override + @SuppressWarnings("NullAway") public long contentLength() throws IOException { verifyExists(); @@ -122,6 +123,7 @@ public boolean exists() { } @Override + @SuppressWarnings("NullAway") public long lastModified() throws IOException { verifyExists(); @@ -139,6 +141,7 @@ public String getDescription() { * @return never {@literal null}. * @throws IllegalStateException if the file does not {@link #exists()}. */ + @SuppressWarnings("NullAway") public Object getId() { Assert.state(exists(), () -> String.format("%s does not exist.", getDescription())); @@ -147,7 +150,8 @@ public Object getId() { } @Override - public Object getFileId() { + @SuppressWarnings("NullAway") + public @Nullable Object getFileId() { Assert.state(exists(), () -> String.format("%s does not exist.", getDescription())); return BsonUtils.toJavaType(getGridFSFile().getId()); @@ -157,8 +161,7 @@ public Object getFileId() { * @return the underlying {@link GridFSFile}. Can be {@literal null} if absent. * @since 2.2 */ - @Nullable - public GridFSFile getGridFSFile() { + public @Nullable GridFSFile getGridFSFile() { return this.file; } @@ -170,6 +173,7 @@ public GridFSFile getGridFSFile() { * provided via {@link GridFSFile}. * @throws IllegalStateException if the file does not {@link #exists()}. */ + @SuppressWarnings("NullAway") public String getContentType() { Assert.state(exists(), () -> String.format("%s does not exist.", getDescription())); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsTemplate.java index 8187c7dbc3..722a57edc1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsTemplate.java @@ -26,13 +26,13 @@ import org.bson.Document; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -167,7 +167,7 @@ public void delete(Query query) { } @Override - public ClassLoader getClassLoader() { + public @Nullable ClassLoader getClassLoader() { return null; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsUpload.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsUpload.java index 9f8d9a47d2..6f2b9ed85b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsUpload.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsUpload.java @@ -20,9 +20,9 @@ import org.bson.Document; import org.bson.types.ObjectId; - +import org.jspecify.annotations.Nullable; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import com.mongodb.client.gridfs.model.GridFSFile; @@ -61,8 +61,7 @@ private GridFsUpload(@Nullable ID id, Lazy dataStream, String filen * @see org.springframework.data.mongodb.gridfs.GridFsObject#getFileId() */ @Override - @Nullable - public ID getFileId() { + public @Nullable ID getFileId() { return id; } @@ -72,6 +71,7 @@ public String getFilename() { } @Override + @SuppressWarnings("NullAway") public InputStream getContent() { return dataStream.orElse(InputStream.nullInputStream()); } @@ -98,9 +98,9 @@ public static GridFsUploadBuilder fromStream(InputStream stream) { */ public static class GridFsUploadBuilder { - private Object id; - private Lazy dataStream; - private String filename; + private @Nullable Object id; + private @Nullable Lazy dataStream; + private @Nullable String filename; private Options options = Options.none(); private GridFsUploadBuilder() {} @@ -124,6 +124,7 @@ public GridFsUploadBuilder content(InputStream stream) { * @param stream the upload content. * @return this. */ + @Contract("_ -> this") public GridFsUploadBuilder content(Supplier stream) { Assert.notNull(stream, "InputStream Supplier must not be null"); @@ -139,6 +140,8 @@ public GridFsUploadBuilder content(Supplier stream) { * @param * @return this. */ + @SuppressWarnings("unchecked") + @Contract("_ -> this") public GridFsUploadBuilder id(T1 id) { this.id = id; @@ -151,6 +154,7 @@ public GridFsUploadBuilder id(T1 id) { * @param filename the filename to use. * @return this. */ + @Contract("_ -> this") public GridFsUploadBuilder filename(String filename) { this.filename = filename; @@ -163,6 +167,7 @@ public GridFsUploadBuilder filename(String filename) { * @param options must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public GridFsUploadBuilder options(Options options) { Assert.notNull(options, "Options must not be null"); @@ -177,6 +182,7 @@ public GridFsUploadBuilder options(Options options) { * @param metadata must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public GridFsUploadBuilder metadata(Document metadata) { this.options = this.options.metadata(metadata); @@ -189,6 +195,7 @@ public GridFsUploadBuilder metadata(Document metadata) { * @param chunkSize use negative number for default. * @return this. */ + @Contract("_ -> this") public GridFsUploadBuilder chunkSize(int chunkSize) { this.options = this.options.chunkSize(chunkSize); @@ -201,6 +208,7 @@ public GridFsUploadBuilder chunkSize(int chunkSize) { * @param gridFSFile must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public GridFsUploadBuilder gridFsFile(GridFSFile gridFSFile) { Assert.notNull(gridFSFile, "GridFSFile must not be null"); @@ -219,13 +227,20 @@ public GridFsUploadBuilder gridFsFile(GridFSFile gridFSFile) { * @param contentType must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public GridFsUploadBuilder contentType(String contentType) { this.options = this.options.contentType(contentType); return this; } + @Contract("-> new") public GridFsUpload build() { + + Assert.notNull(dataStream, "DataStream must be set first"); + Assert.notNull(filename, "Filename must be set first"); + Assert.notNull(options, "Options must be set first"); + return new GridFsUpload(id, dataStream, filename, options); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsOperations.java index 9ee47e0bb9..f8a6bd804f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsOperations.java @@ -20,12 +20,12 @@ import org.bson.Document; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.gridfs.ReactiveGridFsUpload.ReactiveGridFsUploadBuilder; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsResource.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsResource.java index aec7cadef1..e889ec7183 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsResource.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsResource.java @@ -22,6 +22,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.bson.BsonValue; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; @@ -29,7 +30,6 @@ import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.mongodb.client.gridfs.model.GridFSFile; @@ -115,7 +115,7 @@ public static ReactiveGridFsResource absent(String filename) { } @Override - public Object getFileId() { + public @Nullable Object getFileId() { return id instanceof BsonValue bsonValue ? BsonUtils.toJavaType(bsonValue) : id; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsTemplate.java index 305e55aee4..092f81d1fa 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsTemplate.java @@ -26,6 +26,7 @@ import org.bson.BsonValue; import org.bson.Document; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; @@ -37,7 +38,6 @@ import org.springframework.data.mongodb.core.query.SerializationUtils; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsUpload.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsUpload.java index 2f16c3b06e..09ea77798c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsUpload.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsUpload.java @@ -17,9 +17,10 @@ import org.bson.Document; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import com.mongodb.client.gridfs.model.GridFSFile; @@ -58,8 +59,7 @@ private ReactiveGridFsUpload(@Nullable ID id, Publisher dataStream, * @see org.springframework.data.mongodb.gridfs.GridFsObject#getFileId() */ @Override - @Nullable - public ID getFileId() { + public @Nullable ID getFileId() { return id; } @@ -96,8 +96,8 @@ public static ReactiveGridFsUploadBuilder fromPublisher(Publisher { private @Nullable Object id; - private Publisher dataStream; - private String filename; + private @Nullable Publisher dataStream; + private @Nullable String filename; private Options options = Options.none(); private ReactiveGridFsUploadBuilder() {} @@ -108,6 +108,7 @@ private ReactiveGridFsUploadBuilder() {} * @param source the upload content. * @return this. */ + @Contract("_ -> this") public ReactiveGridFsUploadBuilder content(Publisher source) { this.dataStream = source; return this; @@ -120,6 +121,7 @@ public ReactiveGridFsUploadBuilder content(Publisher source) { * @param * @return this. */ + @Contract("_ -> this") public ReactiveGridFsUploadBuilder id(T1 id) { this.id = id; @@ -132,6 +134,7 @@ public ReactiveGridFsUploadBuilder id(T1 id) { * @param filename the filename to use. * @return this. */ + @Contract("_ -> this") public ReactiveGridFsUploadBuilder filename(String filename) { this.filename = filename; @@ -144,6 +147,7 @@ public ReactiveGridFsUploadBuilder filename(String filename) { * @param options must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public ReactiveGridFsUploadBuilder options(Options options) { Assert.notNull(options, "Options must not be null"); @@ -156,8 +160,9 @@ public ReactiveGridFsUploadBuilder options(Options options) { * Set the file metadata. * * @param metadata must not be {@literal null}. - * @return + * @return this. */ + @Contract("_ -> this") public ReactiveGridFsUploadBuilder metadata(Document metadata) { this.options = this.options.metadata(metadata); @@ -168,8 +173,9 @@ public ReactiveGridFsUploadBuilder metadata(Document metadata) { * Set the upload chunk size in bytes. * * @param chunkSize use negative number for default. - * @return + * @return this. */ + @Contract("_ -> this") public ReactiveGridFsUploadBuilder chunkSize(int chunkSize) { this.options = this.options.chunkSize(chunkSize); @@ -182,6 +188,7 @@ public ReactiveGridFsUploadBuilder chunkSize(int chunkSize) { * @param gridFSFile must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public ReactiveGridFsUploadBuilder gridFsFile(GridFSFile gridFSFile) { Assert.notNull(gridFSFile, "GridFSFile must not be null"); @@ -200,13 +207,20 @@ public ReactiveGridFsUploadBuilder gridFsFile(GridFSFile gridFSFile) { * @param contentType must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public ReactiveGridFsUploadBuilder contentType(String contentType) { this.options = this.options.contentType(contentType); return this; } + @Contract("-> new") public ReactiveGridFsUpload build() { + + Assert.notNull(dataStream, "DataStream must be set first"); + Assert.notNull(filename, "Filename must be set first"); + Assert.notNull(options, "Options must be set first"); + return new ReactiveGridFsUpload(id, dataStream, filename, options); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/package-info.java index 2f3b5af150..57726d69cc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/package-info.java @@ -1,6 +1,6 @@ /** * Support for MongoDB GridFS feature. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.gridfs; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/AbstractMonitor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/AbstractMonitor.java deleted file mode 100644 index 5ffe37a4a7..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/AbstractMonitor.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2002-2025 the original author 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.mongodb.monitor; - -import java.util.List; -import java.util.stream.Collectors; - -import org.bson.Document; - -import com.mongodb.ServerAddress; -import com.mongodb.client.MongoClient; -import com.mongodb.client.MongoDatabase; -import com.mongodb.connection.ServerDescription; - -/** - * Base class to encapsulate common configuration settings when connecting to a database - * - * @author Mark Pollack - * @author Oliver Gierke - * @author Christoph Strobl - * @deprecated since 4.5 - */ -@Deprecated(since = "4.5", forRemoval = true) -public abstract class AbstractMonitor { - - private final MongoClient mongoClient; - - /** - * @param mongoClient must not be {@literal null}. - * @since 2.2 - */ - protected AbstractMonitor(MongoClient mongoClient) { - this.mongoClient = mongoClient; - } - - public Document getServerStatus() { - return getDb("admin").runCommand(new Document("serverStatus", 1).append("rangeDeleter", 1).append("repl", 1)); - } - - public MongoDatabase getDb(String databaseName) { - return mongoClient.getDatabase(databaseName); - } - - protected MongoClient getMongoClient() { - return mongoClient; - } - - protected List hosts() { - - return mongoClient.getClusterDescription().getServerDescriptions().stream().map(ServerDescription::getAddress) - .collect(Collectors.toList()); - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/AssertMetrics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/AssertMetrics.java deleted file mode 100644 index 15666fa4d0..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/AssertMetrics.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2002-2025 the original author 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.mongodb.monitor; - -import org.bson.Document; -import org.springframework.jmx.export.annotation.ManagedMetric; -import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.jmx.support.MetricType; - -import com.mongodb.client.MongoClient; - -/** - * JMX Metrics for assertions - * - * @author Mark Pollack - * @deprecated since 4.5 - */ -@Deprecated(since = "4.5", forRemoval = true) -@ManagedResource(description = "Assertion Metrics") -public class AssertMetrics extends AbstractMonitor { - - /** - * @param mongoClient must not be {@literal null}. - * @since 2.2 - */ - public AssertMetrics(MongoClient mongoClient) { - super(mongoClient); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Regular") - public int getRegular() { - return getBtree("regular"); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Warning") - public int getWarning() { - return getBtree("warning"); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Msg") - public int getMsg() { - return getBtree("msg"); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "User") - public int getUser() { - return getBtree("user"); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Rollovers") - public int getRollovers() { - return getBtree("rollovers"); - } - - private int getBtree(String key) { - Document asserts = (Document) getServerStatus().get("asserts"); - // Class c = btree.get(key).getClass(); - return (Integer) asserts.get(key); - } - -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/BackgroundFlushingMetrics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/BackgroundFlushingMetrics.java deleted file mode 100644 index 2ceb75a4f8..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/BackgroundFlushingMetrics.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2002-2025 the original author 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.mongodb.monitor; - -import java.util.Date; - -import org.bson.Document; -import org.springframework.jmx.export.annotation.ManagedMetric; -import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.jmx.support.MetricType; - -import com.mongodb.client.MongoClient; - -/** - * JMX Metrics for Background Flushing - * - * @author Mark Pollack - * @deprecated since 4.5 - */ -@Deprecated(since = "4.5", forRemoval = true) -@ManagedResource(description = "Background Flushing Metrics") -public class BackgroundFlushingMetrics extends AbstractMonitor { - - /** - * @param mongoClient must not be {@literal null}. - * @since 2.2 - */ - public BackgroundFlushingMetrics(MongoClient mongoClient) { - super(mongoClient); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Flushes") - public int getFlushes() { - return getFlushingData("flushes", java.lang.Integer.class); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Total ms", unit = "ms") - public int getTotalMs() { - return getFlushingData("total_ms", java.lang.Integer.class); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Average ms", unit = "ms") - public double getAverageMs() { - return getFlushingData("average_ms", java.lang.Double.class); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Last Ms", unit = "ms") - public int getLastMs() { - return getFlushingData("last_ms", java.lang.Integer.class); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Last finished") - public Date getLastFinished() { - return getLast(); - } - - @SuppressWarnings("unchecked") - private T getFlushingData(String key, Class targetClass) { - Document mem = (Document) getServerStatus().get("backgroundFlushing"); - return (T) mem.get(key); - } - - private Date getLast() { - Document bgFlush = (Document) getServerStatus().get("backgroundFlushing"); - Date lastFinished = (Date) bgFlush.get("last_finished"); - return lastFinished; - } - -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/BtreeIndexCounters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/BtreeIndexCounters.java deleted file mode 100644 index 671d017e05..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/BtreeIndexCounters.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2002-2025 the original author 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.mongodb.monitor; - -import org.bson.Document; -import org.springframework.jmx.export.annotation.ManagedMetric; -import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.jmx.support.MetricType; - -import com.mongodb.client.MongoClient; - -/** - * JMX Metrics for B-tree index counters - * - * @author Mark Pollack - * @deprecated since 4.5 - */ -@Deprecated(since = "4.5", forRemoval = true) -@ManagedResource(description = "Btree Metrics") -public class BtreeIndexCounters extends AbstractMonitor { - - /** - * @param mongoClient must not be {@literal null}. - * @since 2.2 - */ - public BtreeIndexCounters(MongoClient mongoClient) { - super(mongoClient); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Accesses") - public int getAccesses() { - return getBtree("accesses"); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Hits") - public int getHits() { - return getBtree("hits"); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Misses") - public int getMisses() { - return getBtree("misses"); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Resets") - public int getResets() { - return getBtree("resets"); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Miss Ratio") - public int getMissRatio() { - return getBtree("missRatio"); - } - - private int getBtree(String key) { - Document indexCounters = (Document) getServerStatus().get("indexCounters"); - if (indexCounters.get("note") != null) { - String message = (String) indexCounters.get("note"); - if (message.contains("not supported")) { - return -1; - } - } - Document btree = (Document) indexCounters.get("btree"); - // Class c = btree.get(key).getClass(); - return (Integer) btree.get(key); - } - -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/ConnectionMetrics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/ConnectionMetrics.java deleted file mode 100644 index 0d0eb84b35..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/ConnectionMetrics.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2002-2025 the original author 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.mongodb.monitor; - -import org.bson.Document; -import org.springframework.jmx.export.annotation.ManagedMetric; -import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.jmx.support.MetricType; - -import com.mongodb.client.MongoClient; - -/** - * JMX Metrics for Connections - * - * @author Mark Pollack - * @deprecated since 4.5 - */ -@Deprecated(since = "4.5", forRemoval = true) -@ManagedResource(description = "Connection metrics") -public class ConnectionMetrics extends AbstractMonitor { - - /** - * @param mongoClient must not be {@literal null}. - * @since 2.2 - */ - public ConnectionMetrics(MongoClient mongoClient) { - super(mongoClient); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Current Connections") - public int getCurrent() { - return getConnectionData("current", java.lang.Integer.class); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Available Connections") - public int getAvailable() { - return getConnectionData("available", java.lang.Integer.class); - } - - @SuppressWarnings("unchecked") - private T getConnectionData(String key, Class targetClass) { - Document mem = (Document) getServerStatus().get("connections"); - // Class c = mem.get(key).getClass(); - return (T) mem.get(key); - } - -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/GlobalLockMetrics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/GlobalLockMetrics.java deleted file mode 100644 index 6997f5fba8..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/GlobalLockMetrics.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2002-2025 the original author 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.mongodb.monitor; - -import org.bson.Document; -import org.springframework.jmx.export.annotation.ManagedMetric; -import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.jmx.support.MetricType; - -import com.mongodb.DBObject; -import com.mongodb.client.MongoClient; - -/** - * JMX Metrics for Global Locks - * - * @author Mark Pollack - * @deprecated since 4.5 - */ -@Deprecated(since = "4.5", forRemoval = true) -@ManagedResource(description = "Global Lock Metrics") -public class GlobalLockMetrics extends AbstractMonitor { - - /** - * @param mongoClient must not be {@literal null}. - * @since 2.2 - */ - public GlobalLockMetrics(MongoClient mongoClient) { - super(mongoClient); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Total time") - public double getTotalTime() { - return getGlobalLockData("totalTime", java.lang.Double.class); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Lock time", unit = "s") - public double getLockTime() { - return getGlobalLockData("lockTime", java.lang.Double.class); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Lock time") - public double getLockTimeRatio() { - return getGlobalLockData("ratio", java.lang.Double.class); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Current Queue") - public int getCurrentQueueTotal() { - return getCurrentQueue("total"); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Reader Queue") - public int getCurrentQueueReaders() { - return getCurrentQueue("readers"); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Writer Queue") - public int getCurrentQueueWriters() { - return getCurrentQueue("writers"); - } - - @SuppressWarnings("unchecked") - private T getGlobalLockData(String key, Class targetClass) { - DBObject globalLock = (DBObject) getServerStatus().get("globalLock"); - return (T) globalLock.get(key); - } - - private int getCurrentQueue(String key) { - Document globalLock = (Document) getServerStatus().get("globalLock"); - Document currentQueue = (Document) globalLock.get("currentQueue"); - return (Integer) currentQueue.get(key); - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/MemoryMetrics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/MemoryMetrics.java deleted file mode 100644 index 4dbdebb26f..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/MemoryMetrics.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2002-2025 the original author 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.mongodb.monitor; - -import org.bson.Document; -import org.springframework.jmx.export.annotation.ManagedMetric; -import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.jmx.support.MetricType; - -import com.mongodb.client.MongoClient; - -/** - * JMX Metrics for Memory - * - * @author Mark Pollack - * @deprecated since 4.5 - */ -@Deprecated(since = "4.5", forRemoval = true) -@ManagedResource(description = "Memory Metrics") -public class MemoryMetrics extends AbstractMonitor { - - /** - * @param mongoClient - * @since 2.2 - */ - public MemoryMetrics(MongoClient mongoClient) { - super(mongoClient); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Memory address size") - public int getBits() { - return getMemData("bits", java.lang.Integer.class); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Resident in Physical Memory", unit = "MB") - public int getResidentSpace() { - return getMemData("resident", java.lang.Integer.class); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Virtual Address Space", unit = "MB") - public int getVirtualAddressSpace() { - return getMemData("virtual", java.lang.Integer.class); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Is memory info supported on this platform") - public boolean getMemoryInfoSupported() { - return getMemData("supported", java.lang.Boolean.class); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Memory Mapped Space", unit = "MB") - public int getMemoryMappedSpace() { - return getMemData("mapped", java.lang.Integer.class); - } - - @SuppressWarnings("unchecked") - private T getMemData(String key, Class targetClass) { - Document mem = (Document) getServerStatus().get("mem"); - // Class c = mem.get(key).getClass(); - return (T) mem.get(key); - } - -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/OperationCounters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/OperationCounters.java deleted file mode 100644 index 1624501490..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/OperationCounters.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2002-2025 the original author 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.mongodb.monitor; - -import org.bson.Document; -import org.springframework.jmx.export.annotation.ManagedMetric; -import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.jmx.support.MetricType; -import org.springframework.util.NumberUtils; - -import com.mongodb.client.MongoClient; - -/** - * JMX Metrics for Operation counters - * - * @author Mark Pollack - * @deprecated since 4.5 - */ -@Deprecated(since = "4.5", forRemoval = true) -@ManagedResource(description = "Operation Counters") -public class OperationCounters extends AbstractMonitor { - - /** - * @param mongoClient - * @since 2.2 - */ - public OperationCounters(MongoClient mongoClient) { - super(mongoClient); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Insert operation count") - public int getInsertCount() { - return getOpCounter("insert"); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Query operation count") - public int getQueryCount() { - return getOpCounter("query"); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Update operation count") - public int getUpdateCount() { - return getOpCounter("update"); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Delete operation count") - public int getDeleteCount() { - return getOpCounter("delete"); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "GetMore operation count") - public int getGetMoreCount() { - return getOpCounter("getmore"); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Command operation count") - public int getCommandCount() { - return getOpCounter("command"); - } - - private int getOpCounter(String key) { - Document opCounters = (Document) getServerStatus().get("opcounters"); - return NumberUtils.convertNumberToTargetClass((Number) opCounters.get(key), Integer.class); - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/ServerInfo.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/ServerInfo.java deleted file mode 100644 index 3aedf3f29f..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/ServerInfo.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2012-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.monitor; - -import java.net.UnknownHostException; - -import org.springframework.jmx.export.annotation.ManagedMetric; -import org.springframework.jmx.export.annotation.ManagedOperation; -import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.jmx.support.MetricType; -import org.springframework.util.StringUtils; - -import com.mongodb.client.MongoClient; - -/** - * Expose basic server information via JMX - * - * @author Mark Pollack - * @author Thomas Darimont - * @author Christoph Strobl - * @deprecated since 4.5 - */ -@Deprecated(since = "4.5", forRemoval = true) -@ManagedResource(description = "Server Information") -public class ServerInfo extends AbstractMonitor { - - /** - * @param mongoClient - * @since 2.2 - */ - protected ServerInfo(MongoClient mongoClient) { - super(mongoClient); - } - - /** - * Returns the hostname of the used server reported by MongoDB. - * - * @return the reported hostname can also be an IP address. - * @throws UnknownHostException - */ - @ManagedOperation(description = "Server host name") - public String getHostName() throws UnknownHostException { - - /* - * UnknownHostException is not necessary anymore, but clients could have - * called this method in a try..catch(UnknownHostException) already - */ - return StringUtils.collectionToDelimitedString(hosts(), ","); - } - - @ManagedMetric(displayName = "Uptime Estimate") - public double getUptimeEstimate() { - return (Double) getServerStatus().get("uptimeEstimate"); - } - - @ManagedOperation(description = "MongoDB Server Version") - public String getVersion() { - return (String) getServerStatus().get("version"); - } - - @ManagedOperation(description = "Local Time") - public String getLocalTime() { - return (String) getServerStatus().get("localTime"); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Server uptime in seconds", unit = "seconds") - public double getUptime() { - return (Double) getServerStatus().get("uptime"); - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/package-info.java deleted file mode 100644 index 1e1c221b64..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * MongoDB specific JMX monitoring support. - */ -@Deprecated(since = "4.5", forRemoval = true) -@org.springframework.lang.NonNullApi -package org.springframework.data.mongodb.monitor; - diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java index b823ce223b..550a71b301 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java @@ -17,10 +17,8 @@ import io.micrometer.common.KeyValues; -import java.net.InetSocketAddress; - import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames; -import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import com.mongodb.ConnectionString; @@ -67,6 +65,10 @@ public KeyValues getLowCardinalityKeyValues(MongoHandlerContext context) { .and(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue(context.getCollectionName())); } + if(context.getCommandStartedEvent() == null) { + throw new IllegalStateException("not command started event present"); + } + ConnectionDescription connectionDescription = context.getCommandStartedEvent().getConnectionDescription(); if (connectionDescription != null) { @@ -78,16 +80,6 @@ public KeyValues getLowCardinalityKeyValues(MongoHandlerContext context) { keyValues = keyValues.and(LowCardinalityCommandKeyNames.NET_TRANSPORT.withValue("IP.TCP"), LowCardinalityCommandKeyNames.NET_PEER_NAME.withValue(serverAddress.getHost()), LowCardinalityCommandKeyNames.NET_PEER_PORT.withValue("" + serverAddress.getPort())); - - InetSocketAddress socketAddress = MongoCompatibilityAdapter.serverAddressAdapter(serverAddress) - .getSocketAddress(); - - if (socketAddress != null) { - - keyValues = keyValues.and( - LowCardinalityCommandKeyNames.NET_SOCK_PEER_ADDR.withValue(socketAddress.getHostName()), - LowCardinalityCommandKeyNames.NET_SOCK_PEER_PORT.withValue("" + socketAddress.getPort())); - } } ConnectionId connectionId = connectionDescription.getConnectionId(); @@ -111,6 +103,8 @@ public String getContextualName(MongoHandlerContext context) { String collectionName = context.getCollectionName(); CommandStartedEvent commandStartedEvent = context.getCommandStartedEvent(); + Assert.notNull(commandStartedEvent, "CommandStartedEvent must not be null"); + if (ObjectUtils.isEmpty(collectionName)) { return commandStartedEvent.getCommandName(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MapRequestContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MapRequestContext.java index 854e1481fc..6185c95db5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MapRequestContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MapRequestContext.java @@ -17,9 +17,11 @@ import java.util.HashMap; import java.util.Map; +import java.util.NoSuchElementException; import java.util.stream.Stream; import com.mongodb.RequestContext; +import org.jspecify.annotations.Nullable; /** * A {@link Map}-based {@link RequestContext}. @@ -42,7 +44,13 @@ public MapRequestContext(Map context) { @Override public T get(Object key) { - return (T) map.get(key); + + + T value = (T) map.get(key); + if(value != null) { + return value; + } + throw new NoSuchElementException("%s is missing".formatted(key)); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java index cc58aac56e..cab9cd5cb8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java @@ -25,8 +25,7 @@ import org.bson.BsonDocument; import org.bson.BsonValue; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import com.mongodb.ConnectionString; import com.mongodb.RequestContext; @@ -55,12 +54,12 @@ public class MongoHandlerContext extends SenderContext { "killCursors", "listIndexes", "reIndex")); private final @Nullable ConnectionString connectionString; - private final CommandStartedEvent commandStartedEvent; - private final RequestContext requestContext; - private final String collectionName; + private final @Nullable CommandStartedEvent commandStartedEvent; + private final @Nullable RequestContext requestContext; + private final @Nullable String collectionName; - private CommandSucceededEvent commandSucceededEvent; - private CommandFailedEvent commandFailedEvent; + private @Nullable CommandSucceededEvent commandSucceededEvent; + private @Nullable CommandFailedEvent commandFailedEvent; public MongoHandlerContext(@Nullable ConnectionString connectionString, CommandStartedEvent commandStartedEvent, RequestContext requestContext) { @@ -72,28 +71,27 @@ public MongoHandlerContext(@Nullable ConnectionString connectionString, CommandS this.collectionName = getCollectionName(commandStartedEvent); } - public CommandStartedEvent getCommandStartedEvent() { + public @Nullable CommandStartedEvent getCommandStartedEvent() { return this.commandStartedEvent; } - public RequestContext getRequestContext() { + public @Nullable RequestContext getRequestContext() { return this.requestContext; } public String getDatabaseName() { - return commandStartedEvent.getDatabaseName(); + return commandStartedEvent != null ? commandStartedEvent.getDatabaseName() : "n/a"; } - public String getCollectionName() { + public @Nullable String getCollectionName() { return this.collectionName; } public String getCommandName() { - return commandStartedEvent.getCommandName(); + return commandStartedEvent != null ? commandStartedEvent.getCommandName() : "n/a"; } - @Nullable - public ConnectionString getConnectionString() { + public @Nullable ConnectionString getConnectionString() { return connectionString; } @@ -135,8 +133,7 @@ private static String getCollectionName(CommandStartedEvent event) { * * @return trimmed string from {@code bsonValue} or null if the trimmed string was empty or the value wasn't a string */ - @Nullable - private static String getNonEmptyBsonString(@Nullable BsonValue bsonValue) { + private static @Nullable String getNonEmptyBsonString(@Nullable BsonValue bsonValue) { if (bsonValue == null || !bsonValue.isString()) { return null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservationCommandListener.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservationCommandListener.java index 9360a95de2..914396ab96 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservationCommandListener.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservationCommandListener.java @@ -23,7 +23,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import com.mongodb.ConnectionString; @@ -197,8 +197,7 @@ private void doInObservation(@Nullable RequestContext requestContext, * @param context * @return */ - @Nullable - private static Observation observationFromContext(RequestContext context) { + private static @Nullable Observation observationFromContext(RequestContext context) { Observation observation = context.getOrDefault(ObservationThreadLocalAccessor.KEY, null); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/package-info.java index d240e12f9e..d6319e5f4f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/package-info.java @@ -1,5 +1,5 @@ /** * Infrastructure to provide driver observability using Micrometer. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.observability; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/package-info.java index 900342bbcb..989655f4a6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/package-info.java @@ -1,5 +1,5 @@ /** * Spring Data's MongoDB abstraction. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/VectorSearch.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/VectorSearch.java new file mode 100644 index 0000000000..336889f719 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/VectorSearch.java @@ -0,0 +1,123 @@ +/* + * 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.mongodb.repository; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation; + +/** + * Annotation to declare Vector Search queries directly on repository methods. Vector Search queries are used to search + * for similar documents based on vector embeddings typically returning + * {@link org.springframework.data.domain.SearchResults} and limited by either a + * {@link org.springframework.data.domain.Score} (within) or a {@link org.springframework.data.domain.Range} of scores + * (between). + *

+ * Vector search must define an index name using the {@link #indexName()} attribute. The index must be created in the + * MongoDB Atlas cluster before executing the query. Any misspelling of the index name will result in returning no + * results. + *

+ * When using pre-filters, you can either define {@link #filter()} or use query derivation to define the pre-filter. + * {@link org.springframework.data.domain.Vector} and distance parameters are considered once these are present. Vector + * search supports sorting and will consider {@link org.springframework.data.domain.Sort} parameters. + * + * @author Mark Paluch + * @since 5.0 + * @see org.springframework.data.domain.Score + * @see org.springframework.data.domain.Vector + * @see org.springframework.data.domain.SearchResults + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Documented +@Query +@Hint +public @interface VectorSearch { + + /** + * Configuration whether to use + * {@link org.springframework.data.mongodb.core.aggregation.VectorSearchOperation.SearchType#ANN} or + * {@link org.springframework.data.mongodb.core.aggregation.VectorSearchOperation.SearchType#ENN} for the search. + * + * @return the search type to use. + */ + VectorSearchOperation.SearchType searchType() default VectorSearchOperation.SearchType.DEFAULT; + + /** + * Name of the Atlas Vector Search index to use. Atlas Vector Search doesn't return results if you misspell the index + * name or if the specified index doesn't already exist on the cluster. + * + * @return name of the Atlas Vector Search index to use. + */ + @AliasFor(annotation = Hint.class, value = "indexName") + String indexName(); + + /** + * Indexed vector type field to search. This is defaulted from the domain model using the first Vector property found. + * + * @return an empty String by default. + */ + String path() default ""; + + /** + * Takes a MongoDB JSON (MQL) string defining the pre-filter against indexed fields. Supports Value Expressions. Alias + * for {@link VectorSearch#filter}. + * + * @return an empty String by default. + */ + @AliasFor(annotation = Query.class) + String value() default ""; + + /** + * Takes a MongoDB JSON (MQL) string defining the pre-filter against indexed fields. Supports Value Expressions. Alias + * for {@link VectorSearch#value}. + * + * @return an empty String by default. + */ + @AliasFor(annotation = Query.class, value = "value") + String filter() default ""; + + /** + * Number of documents to return in the results. This value can't exceed the value of {@link #numCandidates} if you + * specify {@link #numCandidates}. Limit accepts Value Expressions. A Vector Search method cannot define both, + * {@code limit()} and a {@link org.springframework.data.domain.Limit} parameter. Supports Value Expressions. + * + * @return number of documents to return in the results. + */ + String limit() default ""; + + /** + * Number of nearest neighbors to use during the search. Value must be less than or equal to ({@code <=}) + * {@code 10000}. You can't specify a number less than the {@link #limit() number of documents to return}. We + * recommend that you specify a number at least {@code 20} times higher than the {@link #limit() number of documents + * to return} to increase accuracy. + *

+ * This over-request pattern is the recommended way to trade off latency and recall in your ANN searches, and we + * recommend tuning this parameter based on your specific dataset size and query requirements. Required if the query + * uses + * {@link org.springframework.data.mongodb.core.aggregation.VectorSearchOperation.SearchType#ANN}/{@link org.springframework.data.mongodb.core.aggregation.VectorSearchOperation.SearchType#DEFAULT}. + * Supports Value Expressions. + * + * @return number of nearest neighbors to use during the search. + */ + String numCandidates() default ""; + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationInteraction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationInteraction.java new file mode 100644 index 0000000000..003982daf6 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationInteraction.java @@ -0,0 +1,56 @@ +/* + * 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.mongodb.repository.aot; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.springframework.data.repository.aot.generate.QueryMetadata; + +/** + * An {@link MongoInteraction aggregation interaction}. + * + * @author Christoph Strobl + * @since 5.0 + */ +class AggregationInteraction extends MongoInteraction implements QueryMetadata { + + private final StringAggregation aggregation; + + AggregationInteraction(String[] raw) { + this.aggregation = new StringAggregation(raw); + } + + List stages() { + return Arrays.asList(aggregation.pipeline()); + } + + @Override + InteractionType getExecutionType() { + return InteractionType.AGGREGATION; + } + + @Override + public Map serialize() { + + return Map.of(pipelineSerializationKey(), stages()); + } + + protected String pipelineSerializationKey() { + return "pipeline"; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationUpdateInteraction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationUpdateInteraction.java new file mode 100644 index 0000000000..cc672ed1e9 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationUpdateInteraction.java @@ -0,0 +1,52 @@ +/* + * 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.mongodb.repository.aot; + +import java.util.Map; + +/** + * An {@link MongoInteraction} to execute an aggregation update. + * + * @author Christoph Strobl + * @since 5.0 + */ +class AggregationUpdateInteraction extends AggregationInteraction { + + private final QueryInteraction filter; + + AggregationUpdateInteraction(QueryInteraction filter, String[] raw) { + + super(raw); + this.filter = filter; + } + + QueryInteraction getFilter() { + return filter; + } + + @Override + public Map serialize() { + + Map serialized = filter.serialize(); + serialized.putAll(super.serialize()); + return serialized; + } + + @Override + protected String pipelineSerializationKey() { + return "update-" + super.pipelineSerializationKey(); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotMongoRepositoryPostProcessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotMongoRepositoryPostProcessor.java index d49726f724..324871b475 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotMongoRepositoryPostProcessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotMongoRepositoryPostProcessor.java @@ -15,9 +15,12 @@ */ package org.springframework.data.mongodb.repository.aot; +import org.jspecify.annotations.Nullable; import org.springframework.aot.generate.GenerationContext; +import org.springframework.data.aot.AotContext; import org.springframework.data.mongodb.aot.LazyLoadingProxyAotProcessor; import org.springframework.data.mongodb.aot.MongoAotPredicates; +import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor; import org.springframework.data.util.TypeContributor; @@ -31,7 +34,8 @@ public class AotMongoRepositoryPostProcessor extends RepositoryRegistrationAotPr private final LazyLoadingProxyAotProcessor lazyLoadingProxyAotProcessor = new LazyLoadingProxyAotProcessor(); @Override - protected void contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) { + protected @Nullable RepositoryContributor contribute(AotRepositoryContext repositoryContext, + GenerationContext generationContext) { // do some custom type registration here super.contribute(repositoryContext, generationContext); @@ -39,6 +43,14 @@ protected void contribute(AotRepositoryContext repositoryContext, GenerationCont TypeContributor.contribute(type, it -> true, generationContext); lazyLoadingProxyAotProcessor.registerLazyLoadingProxyIfNeeded(type, generationContext); }); + + boolean enabled = Boolean.parseBoolean( + repositoryContext.getEnvironment().getProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "false")); + if (!enabled) { + return null; + } + + return new MongoRepositoryContributor(repositoryContext); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java new file mode 100644 index 0000000000..17c19ad951 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java @@ -0,0 +1,210 @@ +/* + * 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.mongodb.repository.aot; + +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.bson.conversions.Bson; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Vector; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.Point; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions; +import org.springframework.data.mongodb.core.convert.MongoWriter; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.TextCriteria; +import org.springframework.data.mongodb.core.query.UpdateDefinition; +import org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor; +import org.springframework.data.mongodb.repository.query.MongoParameterAccessor; +import org.springframework.data.mongodb.repository.query.MongoQueryCreator; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.data.util.TypeInformation; + +import com.mongodb.DBRef; + +/** + * @author Christoph Strobl + * @since 5.0 + */ +class AotQueryCreator { + + private MongoMappingContext mappingContext; + + public AotQueryCreator() { + + MongoMappingContext mongoMappingContext = new MongoMappingContext(); + mongoMappingContext.setSimpleTypeHolder( + MongoCustomConversions.create((cfg) -> cfg.useNativeDriverJavaTimeCodecs()).getSimpleTypeHolder()); + mongoMappingContext.setAutoIndexCreation(false); + mongoMappingContext.afterPropertiesSet(); + + this.mappingContext = mongoMappingContext; + } + + @SuppressWarnings("NullAway") + StringQuery createQuery(PartTree partTree, int parameterCount) { + + Query query = new MongoQueryCreator(partTree, + new PlaceholderConvertingParameterAccessor(new PlaceholderParameterAccessor(parameterCount)), mappingContext) + .createQuery(); + + if (partTree.isLimiting()) { + query.limit(partTree.getMaxResults()); + } + return new StringQuery(query); + } + + static class PlaceholderConvertingParameterAccessor extends ConvertingParameterAccessor { + + /** + * Creates a new {@link ConvertingParameterAccessor} with the given {@link MongoWriter} and delegate. + * + * @param delegate must not be {@literal null}. + */ + public PlaceholderConvertingParameterAccessor(PlaceholderParameterAccessor delegate) { + super(PlaceholderWriter.INSTANCE, delegate); + } + } + + @NullUnmarked + enum PlaceholderWriter implements MongoWriter { + + INSTANCE; + + @Override + public @Nullable Object convertToMongoType(@Nullable Object obj, @Nullable TypeInformation typeInformation) { + return obj instanceof Placeholder p ? p.getValue() : obj; + } + + @Override + public DBRef toDBRef(Object object, @Nullable MongoPersistentProperty referringProperty) { + return null; + } + + @Override + public void write(Object source, Bson sink) { + + } + } + + @NullUnmarked + static class PlaceholderParameterAccessor implements MongoParameterAccessor { + + private final List placeholders; + + public PlaceholderParameterAccessor(int parameterCount) { + if (parameterCount == 0) { + placeholders = List.of(); + } else { + placeholders = IntStream.range(0, parameterCount).mapToObj(Placeholder::indexed).collect(Collectors.toList()); + } + } + + @Override + public Range getDistanceRange() { + return null; + } + + @Override + public @Nullable Vector getVector() { + return null; + } + + @Override + public @Nullable Score getScore() { + return null; + } + + @Override + public @Nullable Range getScoreRange() { + return null; + } + + @Override + public @Nullable Point getGeoNearLocation() { + return null; + } + + @Override + public @Nullable TextCriteria getFullText() { + return null; + } + + @Override + public @Nullable Collation getCollation() { + return null; + } + + @Override + public Object[] getValues() { + return placeholders.toArray(); + } + + @Override + public @Nullable UpdateDefinition getUpdate() { + return null; + } + + @Override + public @Nullable ScrollPosition getScrollPosition() { + return null; + } + + @Override + public Pageable getPageable() { + return null; + } + + @Override + public Sort getSort() { + return null; + } + + @Override + public @Nullable Class findDynamicProjection() { + return null; + } + + @Override + public @Nullable Object getBindableValue(int index) { + return placeholders.get(index).getValue(); + } + + @Override + public boolean hasBindableNullValue() { + return false; + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Iterator iterator() { + return ((List) placeholders).iterator(); + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java new file mode 100644 index 0000000000..8e9439e7fa --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java @@ -0,0 +1,154 @@ +/* + * 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.mongodb.repository.aot; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.bson.Document; +import org.jspecify.annotations.Nullable; +import org.springframework.data.mongodb.BindableMongoExpression; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.aggregation.AggregationOperation; +import org.springframework.data.mongodb.core.aggregation.AggregationPipeline; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.data.mongodb.core.mapping.FieldName; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * Support class for MongoDB AOT repository fragments. + * + * @author Christoph Strobl + * @since 5.0 + */ +public class MongoAotRepositoryFragmentSupport { + + private final RepositoryMetadata repositoryMetadata; + private final MongoOperations mongoOperations; + private final MongoConverter mongoConverter; + private final ProjectionFactory projectionFactory; + + protected MongoAotRepositoryFragmentSupport(MongoOperations mongoOperations, + RepositoryFactoryBeanSupport.FragmentCreationContext context) { + this(mongoOperations, context.getRepositoryMetadata(), context.getProjectionFactory()); + } + + protected MongoAotRepositoryFragmentSupport(MongoOperations mongoOperations, RepositoryMetadata repositoryMetadata, + ProjectionFactory projectionFactory) { + + this.mongoOperations = mongoOperations; + this.mongoConverter = mongoOperations.getConverter(); + this.repositoryMetadata = repositoryMetadata; + this.projectionFactory = projectionFactory; + } + + protected Document bindParameters(String source, Object[] parameters) { + return new BindableMongoExpression(source, this.mongoConverter, parameters).toDocument(); + } + + protected BasicQuery createQuery(String queryString, Object[] parameters) { + + Document queryDocument = bindParameters(queryString, parameters); + return new BasicQuery(queryDocument); + } + + protected AggregationPipeline createPipeline(List rawStages) { + + List stages = new ArrayList<>(rawStages.size()); + boolean first = true; + for (Object rawStage : rawStages) { + if (rawStage instanceof Document stageDocument) { + if (first) { + stages.add((ctx) -> ctx.getMappedObject(stageDocument)); + } else { + stages.add((ctx) -> stageDocument); + } + } else if (rawStage instanceof AggregationOperation aggregationOperation) { + stages.add(aggregationOperation); + } else { + throw new RuntimeException("%s cannot be converted to AggregationOperation".formatted(rawStage.getClass())); + } + if (first) { + first = false; + } + } + return new AggregationPipeline(stages); + } + + protected List convertSimpleRawResults(Class targetType, List rawResults) { + + List list = new ArrayList<>(rawResults.size()); + for (Document it : rawResults) { + list.add(extractSimpleTypeResult(it, targetType, mongoConverter)); + } + return list; + } + + protected Object convertSimpleRawResult(Class targetType, Document rawResult) { + return extractSimpleTypeResult(rawResult, targetType, mongoConverter); + } + + private static @Nullable T extractSimpleTypeResult(@Nullable Document source, Class targetType, + MongoConverter converter) { + + if (ObjectUtils.isEmpty(source)) { + return null; + } + + if (source.size() == 1) { + return getPotentiallyConvertedSimpleTypeValue(converter, source.values().iterator().next(), targetType); + } + + Document intermediate = new Document(source); + intermediate.remove(FieldName.ID.name()); + + if (intermediate.size() == 1) { + return getPotentiallyConvertedSimpleTypeValue(converter, intermediate.values().iterator().next(), targetType); + } + + for (Map.Entry entry : intermediate.entrySet()) { + if (entry != null && ClassUtils.isAssignable(targetType, entry.getValue().getClass())) { + return targetType.cast(entry.getValue()); + } + } + + throw new IllegalArgumentException( + String.format("o_O no entry of type %s found in %s.", targetType.getSimpleName(), source.toJson())); + } + + @Nullable + @SuppressWarnings("unchecked") + private static T getPotentiallyConvertedSimpleTypeValue(MongoConverter converter, @Nullable Object value, + Class targetType) { + + if (value == null) { + return null; + } + + if (ClassUtils.isAssignableValue(targetType, value)) { + return (T) value; + } + + return converter.getConversionService().convert(value, targetType); + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java new file mode 100644 index 0000000000..999391f5ec --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java @@ -0,0 +1,850 @@ +/* + * 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.mongodb.repository.aot; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.bson.Document; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; + +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.data.domain.SliceImpl; +import org.springframework.data.domain.Sort.Order; +import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; +import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove; +import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationOptions; +import org.springframework.data.mongodb.core.aggregation.AggregationPipeline; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.core.aggregation.TypedAggregation; +import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.mongodb.core.query.BasicUpdate; +import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.repository.Hint; +import org.springframework.data.mongodb.repository.Meta; +import org.springframework.data.mongodb.repository.ReadPreference; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecution; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagedExecution; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SlicedExecution; +import org.springframework.data.mongodb.repository.query.MongoQueryMethod; +import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; +import org.springframework.data.util.ReflectionUtils; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.CodeBlock.Builder; +import org.springframework.javapoet.TypeName; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.NumberUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * {@link CodeBlock} generator for common tasks. + * + * @author Christoph Strobl + * @since 5.0 + */ +class MongoCodeBlocks { + + private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)"); + + /** + * Builder for generating query parsing {@link CodeBlock}. + * + * @param context + * @param queryMethod + * @return new instance of {@link QueryCodeBlockBuilder}. + */ + static QueryCodeBlockBuilder queryBlockBuilder(AotQueryMethodGenerationContext context, + MongoQueryMethod queryMethod) { + return new QueryCodeBlockBuilder(context, queryMethod); + } + + /** + * Builder for generating finder query execution {@link CodeBlock}. + * + * @param context + * @param queryMethod + * @return + */ + static QueryExecutionCodeBlockBuilder queryExecutionBlockBuilder(AotQueryMethodGenerationContext context, + MongoQueryMethod queryMethod) { + + return new QueryExecutionCodeBlockBuilder(context, queryMethod); + } + + /** + * Builder for generating delete execution {@link CodeBlock}. + * + * @param context + * @param queryMethod + * @return + */ + static DeleteExecutionCodeBlockBuilder deleteExecutionBlockBuilder(AotQueryMethodGenerationContext context, + MongoQueryMethod queryMethod) { + + return new DeleteExecutionCodeBlockBuilder(context, queryMethod); + } + + /** + * Builder for generating update parsing {@link CodeBlock}. + * + * @param context + * @param queryMethod + * @return + */ + static UpdateCodeBlockBuilder updateBlockBuilder(AotQueryMethodGenerationContext context, + MongoQueryMethod queryMethod) { + return new UpdateCodeBlockBuilder(context, queryMethod); + } + + /** + * Builder for generating update execution {@link CodeBlock}. + * + * @param context + * @param queryMethod + * @return + */ + static UpdateExecutionCodeBlockBuilder updateExecutionBlockBuilder(AotQueryMethodGenerationContext context, + MongoQueryMethod queryMethod) { + + return new UpdateExecutionCodeBlockBuilder(context, queryMethod); + } + + /** + * Builder for generating aggregation (pipeline) parsing {@link CodeBlock}. + * + * @param context + * @param queryMethod + * @return + */ + static AggregationCodeBlockBuilder aggregationBlockBuilder(AotQueryMethodGenerationContext context, + MongoQueryMethod queryMethod) { + + return new AggregationCodeBlockBuilder(context, queryMethod); + } + + /** + * Builder for generating aggregation execution {@link CodeBlock}. + * + * @param context + * @param queryMethod + * @return + */ + static AggregationExecutionCodeBlockBuilder aggregationExecutionBlockBuilder(AotQueryMethodGenerationContext context, + MongoQueryMethod queryMethod) { + + return new AggregationExecutionCodeBlockBuilder(context, queryMethod); + } + + @NullUnmarked + static class DeleteExecutionCodeBlockBuilder { + + private final AotQueryMethodGenerationContext context; + private final MongoQueryMethod queryMethod; + private String queryVariableName; + + DeleteExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { + + this.context = context; + this.queryMethod = queryMethod; + } + + DeleteExecutionCodeBlockBuilder referencing(String queryVariableName) { + + this.queryVariableName = queryVariableName; + return this; + } + + CodeBlock build() { + + String mongoOpsRef = context.fieldNameOf(MongoOperations.class); + Builder builder = CodeBlock.builder(); + + Class domainType = context.getRepositoryInformation().getDomainType(); + boolean isProjecting = context.getActualReturnType() != null + && !ObjectUtils.nullSafeEquals(TypeName.get(domainType), context.getActualReturnType()); + + Object actualReturnType = isProjecting ? context.getActualReturnType().getType() : domainType; + + builder.add("\n"); + builder.addStatement("$T<$T> $L = $L.remove($T.class)", ExecutableRemove.class, domainType, + context.localVariable("remover"), mongoOpsRef, domainType); + + DeleteExecution.Type type = DeleteExecution.Type.FIND_AND_REMOVE_ALL; + if (!queryMethod.isCollectionQuery()) { + if (!ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType())) { + type = DeleteExecution.Type.FIND_AND_REMOVE_ONE; + } else { + type = DeleteExecution.Type.ALL; + } + } + + actualReturnType = ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType()) + ? TypeName.get(context.getMethod().getReturnType()) + : queryMethod.isCollectionQuery() ? context.getReturnTypeName() : actualReturnType; + + if (ClassUtils.isVoidType(context.getMethod().getReturnType())) { + builder.addStatement("new $T($L, $T.$L).execute($L)", DeleteExecution.class, context.localVariable("remover"), + DeleteExecution.Type.class, type.name(), queryVariableName); + } else if (context.getMethod().getReturnType() == Optional.class) { + builder.addStatement("return $T.ofNullable(($T) new $T($L, $T.$L).execute($L))", Optional.class, + actualReturnType, DeleteExecution.class, context.localVariable("remover"), DeleteExecution.Type.class, + type.name(), queryVariableName); + } else { + builder.addStatement("return ($T) new $T($L, $T.$L).execute($L)", actualReturnType, DeleteExecution.class, + context.localVariable("remover"), DeleteExecution.Type.class, type.name(), queryVariableName); + } + + return builder.build(); + } + } + + @NullUnmarked + static class UpdateExecutionCodeBlockBuilder { + + private final AotQueryMethodGenerationContext context; + private final MongoQueryMethod queryMethod; + private String queryVariableName; + private String updateVariableName; + + UpdateExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { + + this.context = context; + this.queryMethod = queryMethod; + } + + UpdateExecutionCodeBlockBuilder withFilter(String queryVariableName) { + + this.queryVariableName = queryVariableName; + return this; + } + + UpdateExecutionCodeBlockBuilder referencingUpdate(String updateVariableName) { + + this.updateVariableName = updateVariableName; + return this; + } + + CodeBlock build() { + + String mongoOpsRef = context.fieldNameOf(MongoOperations.class); + Builder builder = CodeBlock.builder(); + + builder.add("\n"); + + String updateReference = updateVariableName; + Class domainType = context.getRepositoryInformation().getDomainType(); + builder.addStatement("$T<$T> $L = $L.update($T.class)", ExecutableUpdate.class, domainType, + context.localVariable("updater"), mongoOpsRef, domainType); + + Class returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType()); + if (ReflectionUtils.isVoid(returnType)) { + builder.addStatement("$L.matching($L).apply($L).all()", context.localVariable("updater"), queryVariableName, + updateReference); + } else if (ClassUtils.isAssignable(Long.class, returnType)) { + builder.addStatement("return $L.matching($L).apply($L).all().getModifiedCount()", + context.localVariable("updater"), queryVariableName, updateReference); + } else { + builder.addStatement("$T $L = $L.matching($L).apply($L).all().getModifiedCount()", Long.class, + context.localVariable("modifiedCount"), context.localVariable("updater"), queryVariableName, + updateReference); + builder.addStatement("return $T.convertNumberToTargetClass($L, $T.class)", NumberUtils.class, + context.localVariable("modifiedCount"), returnType); + } + + return builder.build(); + } + } + + @NullUnmarked + static class AggregationExecutionCodeBlockBuilder { + + private final AotQueryMethodGenerationContext context; + private final MongoQueryMethod queryMethod; + private String aggregationVariableName; + + AggregationExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { + + this.context = context; + this.queryMethod = queryMethod; + } + + AggregationExecutionCodeBlockBuilder referencing(String aggregationVariableName) { + + this.aggregationVariableName = aggregationVariableName; + return this; + } + + CodeBlock build() { + + String mongoOpsRef = context.fieldNameOf(MongoOperations.class); + Builder builder = CodeBlock.builder(); + + builder.add("\n"); + + Class outputType = queryMethod.getReturnedObjectType(); + if (MongoSimpleTypes.HOLDER.isSimpleType(outputType)) { + outputType = Document.class; + } else if (ClassUtils.isAssignable(AggregationResults.class, outputType)) { + outputType = queryMethod.getReturnType().getComponentType().getType(); + } + + if (ReflectionUtils.isVoid(queryMethod.getReturnedObjectType())) { + builder.addStatement("$L.aggregate($L, $T.class)", mongoOpsRef, aggregationVariableName, outputType); + return builder.build(); + } + + if (ClassUtils.isAssignable(AggregationResults.class, context.getMethod().getReturnType())) { + builder.addStatement("return $L.aggregate($L, $T.class)", mongoOpsRef, aggregationVariableName, outputType); + return builder.build(); + } + + if (outputType == Document.class) { + + Class returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType()); + + if (queryMethod.isStreamQuery()) { + + builder.addStatement("$T<$T> $L = $L.aggregateStream($L, $T.class)", Stream.class, Document.class, + context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType); + + builder.addStatement("return $L.map(it -> ($T) convertSimpleRawResult($T.class, it))", + context.localVariable("results"), returnType, returnType); + } else { + + builder.addStatement("$T $L = $L.aggregate($L, $T.class)", AggregationResults.class, + context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType); + + if (!queryMethod.isCollectionQuery()) { + builder.addStatement("return $T.<$T>firstElement(convertSimpleRawResults($T.class, $L.getMappedResults()))", + CollectionUtils.class, returnType, returnType, context.localVariable("results")); + } else { + builder.addStatement("return convertSimpleRawResults($T.class, $L.getMappedResults())", returnType, + context.localVariable("results")); + } + } + } else { + if (queryMethod.isSliceQuery()) { + builder.addStatement("$T $L = $L.aggregate($L, $T.class)", AggregationResults.class, + context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType); + builder.addStatement("boolean $L = $L.getMappedResults().size() > $L.getPageSize()", + context.localVariable("hasNext"), context.localVariable("results"), context.getPageableParameterName()); + builder.addStatement( + "return new $T<>($L ? $L.getMappedResults().subList(0, $L.getPageSize()) : $L.getMappedResults(), $L, $L)", + SliceImpl.class, context.localVariable("hasNext"), context.localVariable("results"), + context.getPageableParameterName(), context.localVariable("results"), context.getPageableParameterName(), + context.localVariable("hasNext")); + } else { + + if (queryMethod.isStreamQuery()) { + builder.addStatement("return $L.aggregateStream($L, $T.class)", mongoOpsRef, aggregationVariableName, + outputType); + } else { + + builder.addStatement("return $L.aggregate($L, $T.class).getMappedResults()", mongoOpsRef, + aggregationVariableName, outputType); + } + } + } + + return builder.build(); + } + } + + @NullUnmarked + static class QueryExecutionCodeBlockBuilder { + + private final AotQueryMethodGenerationContext context; + private final MongoQueryMethod queryMethod; + private QueryInteraction query; + + QueryExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { + + this.context = context; + this.queryMethod = queryMethod; + } + + QueryExecutionCodeBlockBuilder forQuery(QueryInteraction query) { + + this.query = query; + return this; + } + + CodeBlock build() { + + String mongoOpsRef = context.fieldNameOf(MongoOperations.class); + + Builder builder = CodeBlock.builder(); + + boolean isProjecting = context.getReturnedType().isProjecting(); + Class domainType = context.getRepositoryInformation().getDomainType(); + Object actualReturnType = queryMethod.getParameters().hasDynamicProjection() || isProjecting + ? TypeName.get(context.getActualReturnType().getType()) + : domainType; + + builder.add("\n"); + + if (queryMethod.getParameters().hasDynamicProjection()) { + builder.addStatement("$T<$T> $L = $L.query($T.class).as($L)", FindWithQuery.class, actualReturnType, + context.localVariable("finder"), mongoOpsRef, domainType, context.getDynamicProjectionParameterName()); + } else if (isProjecting) { + builder.addStatement("$T<$T> $L = $L.query($T.class).as($T.class)", FindWithQuery.class, actualReturnType, + context.localVariable("finder"), mongoOpsRef, domainType, actualReturnType); + } else { + + builder.addStatement("$T<$T> $L = $L.query($T.class)", FindWithQuery.class, actualReturnType, + context.localVariable("finder"), mongoOpsRef, domainType); + } + + String terminatingMethod; + + if (queryMethod.isCollectionQuery() || queryMethod.isPageQuery() || queryMethod.isSliceQuery()) { + terminatingMethod = "all()"; + } else if (query.isCount()) { + terminatingMethod = "count()"; + } else if (query.isExists()) { + terminatingMethod = "exists()"; + } else if (queryMethod.isStreamQuery()) { + terminatingMethod = "stream()"; + } else { + terminatingMethod = Optional.class.isAssignableFrom(context.getReturnType().toClass()) ? "one()" : "oneValue()"; + } + + if (queryMethod.isPageQuery()) { + builder.addStatement("return new $T($L, $L).execute($L)", PagedExecution.class, context.localVariable("finder"), + context.getPageableParameterName(), query.name()); + } else if (queryMethod.isSliceQuery()) { + builder.addStatement("return new $T($L, $L).execute($L)", SlicedExecution.class, + context.localVariable("finder"), context.getPageableParameterName(), query.name()); + } else if (queryMethod.isScrollQuery()) { + + String scrollPositionParameterName = context.getScrollPositionParameterName(); + + builder.addStatement("return $L.matching($L).scroll($L)", context.localVariable("finder"), query.name(), + scrollPositionParameterName); + } else { + if (query.isCount() && !ClassUtils.isAssignable(Long.class, context.getActualReturnType().getRawClass())) { + + Class returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType()); + builder.addStatement("return $T.convertNumberToTargetClass($L.matching($L).$L, $T.class)", NumberUtils.class, + context.localVariable("finder"), query.name(), terminatingMethod, returnType); + + } else { + builder.addStatement("return $L.matching($L).$L", context.localVariable("finder"), query.name(), + terminatingMethod); + } + } + + return builder.build(); + } + } + + @NullUnmarked + static class AggregationCodeBlockBuilder { + + private final AotQueryMethodGenerationContext context; + private final MongoQueryMethod queryMethod; + + private AggregationInteraction source; + private final List arguments; + private String aggregationVariableName; + private boolean pipelineOnly; + + AggregationCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { + + this.context = context; + this.arguments = context.getBindableParameterNames(); + this.queryMethod = queryMethod; + } + + AggregationCodeBlockBuilder stages(AggregationInteraction aggregation) { + + this.source = aggregation; + return this; + } + + AggregationCodeBlockBuilder usingAggregationVariableName(String aggregationVariableName) { + + this.aggregationVariableName = aggregationVariableName; + return this; + } + + AggregationCodeBlockBuilder pipelineOnly(boolean pipelineOnly) { + + this.pipelineOnly = pipelineOnly; + return this; + } + + CodeBlock build() { + + CodeBlock.Builder builder = CodeBlock.builder(); + builder.add("\n"); + + String pipelineName = context.localVariable(aggregationVariableName + (pipelineOnly ? "" : "Pipeline")); + builder.add(pipeline(pipelineName)); + + if (!pipelineOnly) { + + builder.addStatement("$T<$T> $L = $T.newAggregation($T.class, $L.getOperations())", TypedAggregation.class, + context.getRepositoryInformation().getDomainType(), aggregationVariableName, Aggregation.class, + context.getRepositoryInformation().getDomainType(), pipelineName); + + builder.add(aggregationOptions(aggregationVariableName)); + } + + return builder.build(); + } + + private CodeBlock pipeline(String pipelineVariableName) { + + String sortParameter = context.getSortParameterName(); + String limitParameter = context.getLimitParameterName(); + String pageableParameter = context.getPageableParameterName(); + + boolean mightBeSorted = StringUtils.hasText(sortParameter); + boolean mightBeLimited = StringUtils.hasText(limitParameter); + boolean mightBePaged = StringUtils.hasText(pageableParameter); + + int stageCount = source.stages().size(); + if (mightBeSorted) { + stageCount++; + } + if (mightBeLimited) { + stageCount++; + } + if (mightBePaged) { + stageCount += 3; + } + + Builder builder = CodeBlock.builder(); + builder.add(aggregationStages(context.localVariable("stages"), source.stages(), stageCount, arguments)); + + if (mightBeSorted) { + builder.add(sortingStage(sortParameter)); + } + + if (mightBeLimited) { + builder.add(limitingStage(limitParameter)); + } + + if (mightBePaged) { + builder.add(pagingStage(pageableParameter, queryMethod.isSliceQuery())); + } + + builder.addStatement("$T $L = createPipeline($L)", AggregationPipeline.class, pipelineVariableName, + context.localVariable("stages")); + return builder.build(); + } + + private CodeBlock aggregationOptions(String aggregationVariableName) { + + Builder builder = CodeBlock.builder(); + List options = new ArrayList<>(5); + if (ReflectionUtils.isVoid(queryMethod.getReturnedObjectType())) { + options.add(CodeBlock.of(".skipOutput()")); + } + + MergedAnnotation hintAnnotation = context.getAnnotation(Hint.class); + String hint = hintAnnotation.isPresent() ? hintAnnotation.getString("value") : null; + if (StringUtils.hasText(hint)) { + options.add(CodeBlock.of(".hint($S)", hint)); + } + + MergedAnnotation readPreferenceAnnotation = context.getAnnotation(ReadPreference.class); + String readPreference = readPreferenceAnnotation.isPresent() ? readPreferenceAnnotation.getString("value") : null; + if (StringUtils.hasText(readPreference)) { + options.add(CodeBlock.of(".readPreference($T.valueOf($S))", com.mongodb.ReadPreference.class, readPreference)); + } + + if (queryMethod.hasAnnotatedCollation()) { + options.add(CodeBlock.of(".collation($T.parse($S))", Collation.class, queryMethod.getAnnotatedCollation())); + } + + if (!options.isEmpty()) { + + Builder optionsBuilder = CodeBlock.builder(); + optionsBuilder.add("$T $L = $T.builder()\n", AggregationOptions.class, + context.localVariable("aggregationOptions"), AggregationOptions.class); + optionsBuilder.indent(); + for (CodeBlock optionBlock : options) { + optionsBuilder.add(optionBlock); + optionsBuilder.add("\n"); + } + optionsBuilder.add(".build();\n"); + optionsBuilder.unindent(); + builder.add(optionsBuilder.build()); + + builder.addStatement("$L = $L.withOptions($L)", aggregationVariableName, aggregationVariableName, + context.localVariable("aggregationOptions")); + } + return builder.build(); + } + + private CodeBlock aggregationStages(String stageListVariableName, Iterable stages, int stageCount, + List arguments) { + + Builder builder = CodeBlock.builder(); + builder.addStatement("$T<$T> $L = new $T($L)", List.class, Object.class, stageListVariableName, ArrayList.class, + stageCount); + int stageCounter = 0; + + for (String stage : stages) { + String stageName = context.localVariable("stage_%s".formatted(stageCounter++)); + builder.add(renderExpressionToDocument(stage, stageName, arguments)); + builder.addStatement("$L.add($L)", context.localVariable("stages"), stageName); + } + + return builder.build(); + } + + private CodeBlock sortingStage(String sortProvider) { + + Builder builder = CodeBlock.builder(); + + builder.beginControlFlow("if ($L.isSorted())", sortProvider); + builder.addStatement("$T $L = new $T()", Document.class, context.localVariable("sortDocument"), Document.class); + builder.beginControlFlow("for ($T $L : $L)", Order.class, context.localVariable("order"), sortProvider); + builder.addStatement("$L.append($L.getProperty(), $L.isAscending() ? 1 : -1);", + context.localVariable("sortDocument"), context.localVariable("order"), context.localVariable("order")); + builder.endControlFlow(); + builder.addStatement("stages.add(new $T($S, $L))", Document.class, "$sort", + context.localVariable("sortDocument")); + builder.endControlFlow(); + + return builder.build(); + } + + private CodeBlock pagingStage(String pageableProvider, boolean slice) { + + Builder builder = CodeBlock.builder(); + + builder.add(sortingStage(pageableProvider + ".getSort()")); + + builder.beginControlFlow("if ($L.isPaged())", pageableProvider); + builder.beginControlFlow("if ($L.getOffset() > 0)", pageableProvider); + builder.addStatement("$L.add($T.skip($L.getOffset()))", context.localVariable("stages"), Aggregation.class, + pageableProvider); + builder.endControlFlow(); + if (slice) { + builder.addStatement("$L.add($T.limit($L.getPageSize() + 1))", context.localVariable("stages"), + Aggregation.class, pageableProvider); + } else { + builder.addStatement("$L.add($T.limit($L.getPageSize()))", context.localVariable("stages"), Aggregation.class, + pageableProvider); + } + builder.endControlFlow(); + + return builder.build(); + } + + private CodeBlock limitingStage(String limitProvider) { + + Builder builder = CodeBlock.builder(); + + builder.beginControlFlow("if ($L.isLimited())", limitProvider); + builder.addStatement("$L.add($T.limit($L.max()))", context.localVariable("stages"), Aggregation.class, + limitProvider); + builder.endControlFlow(); + + return builder.build(); + } + + } + + @NullUnmarked + static class QueryCodeBlockBuilder { + + private final AotQueryMethodGenerationContext context; + private final MongoQueryMethod queryMethod; + + private QueryInteraction source; + private final List arguments; + private String queryVariableName; + + QueryCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { + + this.context = context; + this.arguments = context.getBindableParameterNames(); + this.queryMethod = queryMethod; + } + + QueryCodeBlockBuilder filter(QueryInteraction query) { + + this.source = query; + return this; + } + + QueryCodeBlockBuilder usingQueryVariableName(String queryVariableName) { + this.queryVariableName = queryVariableName; + return this; + } + + CodeBlock build() { + + CodeBlock.Builder builder = CodeBlock.builder(); + + builder.add("\n"); + builder.add(renderExpressionToQuery(source.getQuery().getQueryString(), queryVariableName)); + + if (StringUtils.hasText(source.getQuery().getFieldsString())) { + + builder.add(renderExpressionToDocument(source.getQuery().getFieldsString(), "fields", arguments)); + builder.addStatement("$L.setFieldsObject(fields)", queryVariableName); + } + + String sortParameter = context.getSortParameterName(); + if (StringUtils.hasText(sortParameter)) { + builder.addStatement("$L.with($L)", queryVariableName, sortParameter); + } else if (StringUtils.hasText(source.getQuery().getSortString())) { + + builder.add(renderExpressionToDocument(source.getQuery().getSortString(), "sort", arguments)); + builder.addStatement("$L.setSortObject(sort)", queryVariableName); + } + + String limitParameter = context.getLimitParameterName(); + if (StringUtils.hasText(limitParameter)) { + builder.addStatement("$L.limit($L)", queryVariableName, limitParameter); + } else if (context.getPageableParameterName() == null && source.getQuery().isLimited()) { + builder.addStatement("$L.limit($L)", queryVariableName, source.getQuery().getLimit()); + } + + String pageableParameter = context.getPageableParameterName(); + if (StringUtils.hasText(pageableParameter) && !queryMethod.isPageQuery() && !queryMethod.isSliceQuery()) { + builder.addStatement("$L.with($L)", queryVariableName, pageableParameter); + } + + MergedAnnotation hintAnnotation = context.getAnnotation(Hint.class); + String hint = hintAnnotation.isPresent() ? hintAnnotation.getString("value") : null; + + if (StringUtils.hasText(hint)) { + builder.addStatement("$L.withHint($S)", queryVariableName, hint); + } + + MergedAnnotation readPreferenceAnnotation = context.getAnnotation(ReadPreference.class); + String readPreference = readPreferenceAnnotation.isPresent() ? readPreferenceAnnotation.getString("value") : null; + + if (StringUtils.hasText(readPreference)) { + builder.addStatement("$L.withReadPreference($T.valueOf($S))", queryVariableName, + com.mongodb.ReadPreference.class, readPreference); + } + + MergedAnnotation metaAnnotation = context.getAnnotation(Meta.class); + + if (metaAnnotation.isPresent()) { + + long maxExecutionTimeMs = metaAnnotation.getLong("maxExecutionTimeMs"); + if (maxExecutionTimeMs != -1) { + builder.addStatement("$L.maxTimeMsec($L)", queryVariableName, maxExecutionTimeMs); + } + + int cursorBatchSize = metaAnnotation.getInt("cursorBatchSize"); + if (cursorBatchSize != 0) { + builder.addStatement("$L.cursorBatchSize($L)", queryVariableName, cursorBatchSize); + } + + String comment = metaAnnotation.getString("comment"); + if (StringUtils.hasText("comment")) { + builder.addStatement("$L.comment($S)", queryVariableName, comment); + } + } + + // TODO: Meta annotation: Disk usage + + return builder.build(); + } + + private CodeBlock renderExpressionToQuery(@Nullable String source, String variableName) { + + Builder builder = CodeBlock.builder(); + if (!StringUtils.hasText(source)) { + + builder.addStatement("$T $L = new $T(new $T())", BasicQuery.class, variableName, BasicQuery.class, + Document.class); + } else if (!containsPlaceholder(source)) { + builder.addStatement("$T $L = new $T($T.parse($S))", BasicQuery.class, variableName, BasicQuery.class, + Document.class, source); + } else { + builder.addStatement("$T $L = createQuery($S, new $T[]{ $L })", BasicQuery.class, variableName, source, + Object.class, StringUtils.collectionToDelimitedString(arguments, ", ")); + } + + return builder.build(); + } + } + + @NullUnmarked + static class UpdateCodeBlockBuilder { + + private UpdateInteraction source; + private List arguments; + private String updateVariableName; + + public UpdateCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { + this.arguments = context.getBindableParameterNames(); + } + + public UpdateCodeBlockBuilder update(UpdateInteraction update) { + this.source = update; + return this; + } + + public UpdateCodeBlockBuilder usingUpdateVariableName(String updateVariableName) { + this.updateVariableName = updateVariableName; + return this; + } + + CodeBlock build() { + + CodeBlock.Builder builder = CodeBlock.builder(); + + builder.add("\n"); + String tmpVariableName = updateVariableName + "Document"; + builder.add(renderExpressionToDocument(source.getUpdate().getUpdateString(), tmpVariableName, arguments)); + builder.addStatement("$T $L = new $T($L)", BasicUpdate.class, updateVariableName, BasicUpdate.class, + tmpVariableName); + + return builder.build(); + } + } + + private static CodeBlock renderExpressionToDocument(@Nullable String source, String variableName, + List arguments) { + + Builder builder = CodeBlock.builder(); + if (!StringUtils.hasText(source)) { + builder.addStatement("$T $L = new $T()", Document.class, variableName, Document.class); + } else if (!containsPlaceholder(source)) { + builder.addStatement("$T $L = $T.parse($S)", Document.class, variableName, Document.class, source); + } else { + builder.addStatement("$T $L = bindParameters($S, new $T[]{ $L })", Document.class, variableName, source, + Object.class, StringUtils.collectionToDelimitedString(arguments, ", ")); + } + return builder.build(); + } + + private static boolean containsPlaceholder(String source) { + return PARAMETER_BINDING_PATTERN.matcher(source).find(); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoInteraction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoInteraction.java new file mode 100644 index 0000000000..fa9ca2f99e --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoInteraction.java @@ -0,0 +1,62 @@ +/* + * 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.mongodb.repository.aot; + +/** + * Base abstraction for interactions with MongoDB. + * + * @author Christoph Strobl + * @since 5.0 + */ +abstract class MongoInteraction { + + abstract InteractionType getExecutionType(); + + boolean isAggregation() { + return InteractionType.AGGREGATION.equals(getExecutionType()); + } + + boolean isCount() { + return InteractionType.COUNT.equals(getExecutionType()); + } + + boolean isDelete() { + return InteractionType.DELETE.equals(getExecutionType()); + } + + boolean isExists() { + return InteractionType.EXISTS.equals(getExecutionType()); + } + + boolean isUpdate() { + return InteractionType.UPDATE.equals(getExecutionType()); + } + + String name() { + + if (isDelete()) { + return "deleteQuery"; + } + if (isCount()) { + return "countQuery"; + } + return "filterQuery"; + } + + enum InteractionType { + QUERY, COUNT, DELETE, EXISTS, UPDATE, AGGREGATION + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java new file mode 100644 index 0000000000..424d067d74 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java @@ -0,0 +1,283 @@ +/* + * 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.mongodb.repository.aot; + +import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.*; + +import java.lang.reflect.Method; +import java.util.Locale; +import java.util.regex.Pattern; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.data.mongodb.repository.Update; +import org.springframework.data.mongodb.repository.query.MongoQueryMethod; +import org.springframework.data.repository.aot.generate.AotRepositoryClassBuilder; +import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder; +import org.springframework.data.repository.aot.generate.MethodContributor; +import org.springframework.data.repository.aot.generate.RepositoryContributor; +import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.TypeName; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * MongoDB specific {@link RepositoryContributor}. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 5.0 + */ +public class MongoRepositoryContributor extends RepositoryContributor { + + private static final Log logger = LogFactory.getLog(MongoRepositoryContributor.class); + + private final AotQueryCreator queryCreator; + private final MongoMappingContext mappingContext; + + public MongoRepositoryContributor(AotRepositoryContext repositoryContext) { + + super(repositoryContext); + this.queryCreator = new AotQueryCreator(); + this.mappingContext = new MongoMappingContext(); + } + + @Override + protected void customizeClass(AotRepositoryClassBuilder classBuilder) { + classBuilder.customize(builder -> builder.superclass(TypeName.get(MongoAotRepositoryFragmentSupport.class))); + } + + @Override + protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) { + + constructorBuilder.addParameter("operations", TypeName.get(MongoOperations.class)); + constructorBuilder.addParameter("context", TypeName.get(RepositoryFactoryBeanSupport.FragmentCreationContext.class), + false); + + constructorBuilder.customize((builder) -> { + builder.addStatement("super(operations, context)"); + }); + } + + @Override + @SuppressWarnings("NullAway") + protected @Nullable MethodContributor contributeQueryMethod(Method method) { + + MongoQueryMethod queryMethod = new MongoQueryMethod(method, getRepositoryInformation(), getProjectionFactory(), + mappingContext); + + if (queryMethod.hasAnnotatedAggregation()) { + AggregationInteraction aggregation = new AggregationInteraction(queryMethod.getAnnotatedAggregation()); + return aggregationMethodContributor(queryMethod, aggregation); + } + + QueryInteraction query = createStringQuery(getRepositoryInformation(), queryMethod, + AnnotatedElementUtils.findMergedAnnotation(method, Query.class), method.getParameterCount()); + + if (queryMethod.hasAnnotatedQuery()) { + if (StringUtils.hasText(queryMethod.getAnnotatedQuery()) + && Pattern.compile("[\\?:][#$]\\{.*\\}").matcher(queryMethod.getAnnotatedQuery()).find()) { + + if (logger.isDebugEnabled()) { + logger.debug( + "Skipping AOT generation for [%s]. SpEL expressions are not supported".formatted(method.getName())); + } + return MethodContributor.forQueryMethod(queryMethod).metadataOnly(query); + } + } + + if (backoff(queryMethod)) { + return null; + } + + if (query.isDelete()) { + return deleteMethodContributor(queryMethod, query); + } + + if (queryMethod.isModifyingQuery()) { + + int updateIndex = queryMethod.getParameters().getUpdateIndex(); + if (updateIndex != -1) { + + UpdateInteraction update = new UpdateInteraction(query, null, updateIndex); + return updateMethodContributor(queryMethod, update); + + } else { + Update updateSource = queryMethod.getUpdateSource(); + if (StringUtils.hasText(updateSource.value())) { + UpdateInteraction update = new UpdateInteraction(query, new StringUpdate(updateSource.value()), null); + return updateMethodContributor(queryMethod, update); + } + + if (!ObjectUtils.isEmpty(updateSource.pipeline())) { + AggregationUpdateInteraction update = new AggregationUpdateInteraction(query, updateSource.pipeline()); + return aggregationUpdateMethodContributor(queryMethod, update); + } + } + } + + return queryMethodContributor(queryMethod, query); + } + + @SuppressWarnings("NullAway") + private QueryInteraction createStringQuery(RepositoryInformation repositoryInformation, MongoQueryMethod queryMethod, + @Nullable Query queryAnnotation, int parameterCount) { + + QueryInteraction query; + if (queryMethod.hasAnnotatedQuery() && queryAnnotation != null) { + query = new QueryInteraction(new StringQuery(queryMethod.getAnnotatedQuery()), queryAnnotation.count(), + queryAnnotation.delete(), queryAnnotation.exists()); + } else { + + PartTree partTree = new PartTree(queryMethod.getName(), repositoryInformation.getDomainType()); + query = new QueryInteraction(queryCreator.createQuery(partTree, parameterCount), partTree.isCountProjection(), + partTree.isDelete(), partTree.isExistsProjection()); + } + + if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.sort())) { + query = query.withSort(queryAnnotation.sort()); + } + if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.fields())) { + query = query.withFields(queryAnnotation.fields()); + } + + return query; + } + + private static boolean backoff(MongoQueryMethod method) { + + // TODO: namedQuery, Regex queries, queries accepting Shapes (e.g. within) or returning arrays. + boolean skip = method.isGeoNearQuery() || method.isSearchQuery() + || method.getName().toLowerCase(Locale.ROOT).contains("regex") || method.getReturnType().getType().isArray(); + + if (skip && logger.isDebugEnabled()) { + logger.debug("Skipping AOT generation for [%s]. Method is either returning an array or a geo-near, regex query" + .formatted(method.getName())); + } + return skip; + } + + private static MethodContributor aggregationMethodContributor(MongoQueryMethod queryMethod, + AggregationInteraction aggregation) { + + return MethodContributor.forQueryMethod(queryMethod).withMetadata(aggregation).contribute(context -> { + + CodeBlock.Builder builder = CodeBlock.builder(); + + builder.add(aggregationBlockBuilder(context, queryMethod).stages(aggregation) + .usingAggregationVariableName("aggregation").build()); + builder.add(aggregationExecutionBlockBuilder(context, queryMethod).referencing("aggregation").build()); + + return builder.build(); + }); + } + + private static MethodContributor updateMethodContributor(MongoQueryMethod queryMethod, + UpdateInteraction update) { + + return MethodContributor.forQueryMethod(queryMethod).withMetadata(update).contribute(context -> { + + CodeBlock.Builder builder = CodeBlock.builder(); + + // update filter + String filterVariableName = context.localVariable(update.name()); + builder.add(queryBlockBuilder(context, queryMethod).filter(update.getFilter()) + .usingQueryVariableName(filterVariableName).build()); + + // update definition + String updateVariableName; + + if (update.hasUpdateDefinitionParameter()) { + updateVariableName = context.getParameterName(update.getRequiredUpdateDefinitionParameter()); + } else { + updateVariableName = context.localVariable("updateDefinition"); + builder.add(updateBlockBuilder(context, queryMethod).update(update).usingUpdateVariableName(updateVariableName) + .build()); + } + + builder.add(updateExecutionBlockBuilder(context, queryMethod).withFilter(filterVariableName) + .referencingUpdate(updateVariableName).build()); + return builder.build(); + }); + } + + private static MethodContributor aggregationUpdateMethodContributor(MongoQueryMethod queryMethod, + AggregationUpdateInteraction update) { + + return MethodContributor.forQueryMethod(queryMethod).withMetadata(update).contribute(context -> { + + CodeBlock.Builder builder = CodeBlock.builder(); + + // update filter + String filterVariableName = context.localVariable(update.name()); + QueryCodeBlockBuilder queryCodeBlockBuilder = queryBlockBuilder(context, queryMethod).filter(update.getFilter()); + builder.add(queryCodeBlockBuilder.usingQueryVariableName(filterVariableName).build()); + + // update definition + String updateVariableName = "updateDefinition"; + builder.add(aggregationBlockBuilder(context, queryMethod).stages(update) + .usingAggregationVariableName(updateVariableName).pipelineOnly(true).build()); + + builder.addStatement("$T $L = $T.from($L.getOperations())", AggregationUpdate.class, + context.localVariable("aggregationUpdate"), AggregationUpdate.class, updateVariableName); + + builder.add(updateExecutionBlockBuilder(context, queryMethod).withFilter(filterVariableName) + .referencingUpdate(context.localVariable("aggregationUpdate")).build()); + return builder.build(); + }); + } + + private static MethodContributor deleteMethodContributor(MongoQueryMethod queryMethod, + QueryInteraction query) { + + return MethodContributor.forQueryMethod(queryMethod).withMetadata(query).contribute(context -> { + + CodeBlock.Builder builder = CodeBlock.builder(); + QueryCodeBlockBuilder queryCodeBlockBuilder = queryBlockBuilder(context, queryMethod).filter(query); + + String queryVariableName = context.localVariable(query.name()); + builder.add(queryCodeBlockBuilder.usingQueryVariableName(queryVariableName).build()); + builder.add(deleteExecutionBlockBuilder(context, queryMethod).referencing(queryVariableName).build()); + return builder.build(); + }); + } + + private static MethodContributor queryMethodContributor(MongoQueryMethod queryMethod, + QueryInteraction query) { + + return MethodContributor.forQueryMethod(queryMethod).withMetadata(query).contribute(context -> { + + CodeBlock.Builder builder = CodeBlock.builder(); + QueryCodeBlockBuilder queryCodeBlockBuilder = queryBlockBuilder(context, queryMethod).filter(query); + + builder.add(queryCodeBlockBuilder.usingQueryVariableName(context.localVariable(query.name())).build()); + builder.add(queryExecutionBlockBuilder(context, queryMethod).forQuery(query).build()); + return builder.build(); + }); + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryInteraction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryInteraction.java new file mode 100644 index 0000000000..563079c03b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryInteraction.java @@ -0,0 +1,83 @@ +/* + * 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.mongodb.repository.aot; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.data.repository.aot.generate.QueryMetadata; +import org.springframework.util.StringUtils; + +/** + * An {@link MongoInteraction} to execute a query. + * + * @author Christoph Strobl + * @since 5.0 + */ +class QueryInteraction extends MongoInteraction implements QueryMetadata { + + private final StringQuery query; + private final InteractionType interactionType; + + QueryInteraction(StringQuery query, boolean count, boolean delete, boolean exists) { + + this.query = query; + if (count) { + interactionType = InteractionType.COUNT; + } else if (exists) { + interactionType = InteractionType.EXISTS; + } else if (delete) { + interactionType = InteractionType.DELETE; + } else { + interactionType = InteractionType.QUERY; + } + } + + StringQuery getQuery() { + return query; + } + + QueryInteraction withSort(String sort) { + query.sort(sort); + return this; + } + + QueryInteraction withFields(String fields) { + query.fields(fields); + return this; + } + + @Override + InteractionType getExecutionType() { + return interactionType; + } + + @Override + public Map serialize() { + + Map serialized = new LinkedHashMap<>(); + + serialized.put("filter", query.getQueryString()); + if (query.isSorted()) { + serialized.put("sort", query.getSortString()); + } + if (StringUtils.hasText(query.getFieldsString())) { + serialized.put("fields", query.getFieldsString()); + } + + return serialized; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/RepositoryRuntimeHints.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/RepositoryRuntimeHints.java index b1ba6ea3f0..00ff498731 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/RepositoryRuntimeHints.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/RepositoryRuntimeHints.java @@ -19,6 +19,7 @@ import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -28,7 +29,6 @@ import org.springframework.data.mongodb.repository.support.QuerydslMongoPredicateExecutor; import org.springframework.data.mongodb.repository.support.ReactiveQuerydslMongoPredicateExecutor; import org.springframework.data.querydsl.QuerydslUtils; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringAggregation.java new file mode 100644 index 0000000000..7b73215e98 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringAggregation.java @@ -0,0 +1,26 @@ +/* + * 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.mongodb.repository.aot; + +/** + * Value object holding the raw representation of an Aggregation Pipeline. + * + * @author Christoph Strobl + * @since 5.0 + */ +record StringAggregation(String[] pipeline) { + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringQuery.java new file mode 100644 index 0000000000..d037198bba --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringQuery.java @@ -0,0 +1,198 @@ +/* + * 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.mongodb.repository.aot; + +import java.util.Optional; +import java.util.Set; + +import org.bson.Document; +import org.jspecify.annotations.Nullable; +import org.springframework.data.domain.KeysetScrollPosition; +import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.core.query.Field; +import org.springframework.data.mongodb.core.query.Meta; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.util.BsonUtils; +import org.springframework.util.StringUtils; + +import com.mongodb.ReadConcern; +import com.mongodb.ReadPreference; + +/** + * Helper to capture setting for AOT queries. + * + * @author Christoph Strobl + * @since 5.0 + */ +class StringQuery extends Query { + + private Query delegate; + private @Nullable String raw; + private @Nullable String sort; + private @Nullable String fields; + + public StringQuery(Query query) { + this.delegate = query; + } + + public StringQuery(String query) { + this.delegate = new Query(); + this.raw = query; + } + + @Nullable + String getQueryString() { + + if (StringUtils.hasText(raw)) { + return raw; + } + + Document queryObj = getQueryObject(); + if (queryObj.isEmpty()) { + return null; + } + return toJson(queryObj); + } + + public Query sort(String sort) { + this.sort = sort; + return this; + } + + @Override + public Field fields() { + return delegate.fields(); + } + + @Override + public boolean hasReadConcern() { + return delegate.hasReadConcern(); + } + + @Override + public @Nullable ReadConcern getReadConcern() { + return delegate.getReadConcern(); + } + + @Override + public boolean hasReadPreference() { + return delegate.hasReadPreference(); + } + + @Override + public @Nullable ReadPreference getReadPreference() { + return delegate.getReadPreference(); + } + + @Override + public boolean hasKeyset() { + return delegate.hasKeyset(); + } + + @Override + public @Nullable KeysetScrollPosition getKeyset() { + return delegate.getKeyset(); + } + + @Override + public Set> getRestrictedTypes() { + return delegate.getRestrictedTypes(); + } + + @Override + public Document getQueryObject() { + return delegate.getQueryObject(); + } + + @Override + public Document getFieldsObject() { + return delegate.getFieldsObject(); + } + + @Override + public Document getSortObject() { + return delegate.getSortObject(); + } + + @Override + public boolean isSorted() { + return delegate.isSorted() || StringUtils.hasText(sort); + } + + @Override + public long getSkip() { + return delegate.getSkip(); + } + + @Override + public boolean isLimited() { + return delegate.isLimited(); + } + + @Override + public int getLimit() { + return delegate.getLimit(); + } + + @Override + public @Nullable String getHint() { + return delegate.getHint(); + } + + @Override + public Meta getMeta() { + return delegate.getMeta(); + } + + @Override + public Optional getCollation() { + return delegate.getCollation(); + } + + @Nullable + String getSortString() { + if (StringUtils.hasText(sort)) { + return sort; + } + Document sort = getSortObject(); + if (sort.isEmpty()) { + return null; + } + return toJson(sort); + } + + @Nullable + String getFieldsString() { + if (StringUtils.hasText(fields)) { + return fields; + } + + Document fields = getFieldsObject(); + if (fields.isEmpty()) { + return null; + } + return toJson(fields); + } + + StringQuery fields(String fields) { + this.fields = fields; + return this; + } + + String toJson(Document source) { + return BsonUtils.writeJson(source).toJsonString(); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringUpdate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringUpdate.java new file mode 100644 index 0000000000..f65ee7912f --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringUpdate.java @@ -0,0 +1,27 @@ +/* + * 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.mongodb.repository.aot; + +/** + * @author Christoph Strobl + * @since 5.0 + */ +record StringUpdate(String raw) { + + String getUpdateString() { + return raw; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/UpdateInteraction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/UpdateInteraction.java new file mode 100644 index 0000000000..525a4782a5 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/UpdateInteraction.java @@ -0,0 +1,82 @@ +/* + * 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.mongodb.repository.aot; + +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +import org.springframework.data.repository.aot.generate.QueryMetadata; +import org.springframework.util.Assert; + +/** + * An {@link MongoInteraction} to execute an update. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 5.0 + */ +class UpdateInteraction extends MongoInteraction implements QueryMetadata { + + private final QueryInteraction filter; + private final @Nullable StringUpdate update; + private final @Nullable Integer updateDefinitionParameter; + + UpdateInteraction(QueryInteraction filter, @Nullable StringUpdate update, + @Nullable Integer updateDefinitionParameter) { + this.filter = filter; + this.update = update; + this.updateDefinitionParameter = updateDefinitionParameter; + } + + public QueryInteraction getFilter() { + return filter; + } + + public @Nullable StringUpdate getUpdate() { + return update; + } + + public int getRequiredUpdateDefinitionParameter() { + + Assert.notNull(updateDefinitionParameter, "UpdateDefinitionParameter must not be null!"); + + return updateDefinitionParameter; + } + + public boolean hasUpdateDefinitionParameter() { + return updateDefinitionParameter != null; + } + + @Override + public Map serialize() { + + Map serialized = filter.serialize(); + + if (update != null) { + serialized.put("filter", filter.getQuery().getQueryString()); + serialized.put("update", update.getUpdateString()); + } + + return serialized; + } + + @Override + InteractionType getExecutionType() { + return InteractionType.UPDATE; + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/package-info.java index 9016519d9b..750cc38678 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/package-info.java @@ -1,5 +1,5 @@ /** * Ahead-Of-Time processors for MongoDB repositories. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.repository.aot; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/cdi/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/cdi/package-info.java index a2cbf659dd..db7edc05bd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/cdi/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/cdi/package-info.java @@ -1,6 +1,6 @@ /** * CDI support for MongoDB specific repository implementation. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.repository.cdi; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtension.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtension.java index 9db7be0069..48b4000750 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtension.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtension.java @@ -23,10 +23,11 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.data.config.ParsingUtils; -import org.springframework.data.mongodb.repository.aot.AotMongoRepositoryPostProcessor; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.aot.AotMongoRepositoryPostProcessor; import org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean; +import org.springframework.data.mongodb.repository.support.SimpleMongoRepository; import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; import org.springframework.data.repository.config.XmlRepositoryConfigurationSource; @@ -55,6 +56,12 @@ public String getModulePrefix() { return "mongo"; } + @Override + public String getRepositoryBaseClassName() { + return SimpleMongoRepository.class.getName(); + } + + @Override public String getRepositoryFactoryBeanClassName() { return MongoRepositoryFactoryBean.class.getName(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtension.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtension.java index 817cc397c2..457e889bef 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtension.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtension.java @@ -23,10 +23,12 @@ import org.springframework.data.config.ParsingUtils; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.data.mongodb.repository.support.ReactiveMongoRepositoryFactoryBean; +import org.springframework.data.mongodb.repository.support.SimpleReactiveMongoRepository; import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; import org.springframework.data.repository.config.RepositoryConfigurationExtension; import org.springframework.data.repository.config.XmlRepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryMetadata; + import org.w3c.dom.Element; /** @@ -47,7 +49,13 @@ public String getModuleName() { return "Reactive MongoDB"; } - public String getRepositoryFactoryClassName() { + @Override + public String getRepositoryBaseClassName() { + return SimpleReactiveMongoRepository.class.getName(); + } + + @Override + public String getRepositoryFactoryBeanClassName() { return ReactiveMongoRepositoryFactoryBean.class.getName(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/package-info.java index d0d9b07081..e276d4d1e0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/package-info.java @@ -1,6 +1,6 @@ /** * Support infrastructure for the configuration of MongoDB specific repositories. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.repository.config; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/package-info.java index 8deddfe939..799597e19c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/package-info.java @@ -1,6 +1,6 @@ /** * MongoDB specific repository implementation. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.repository; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java index 4d0d604a27..596b895ebd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java @@ -20,16 +20,14 @@ import org.bson.Document; import org.bson.codecs.configuration.CodecRegistry; - -import org.springframework.core.env.StandardEnvironment; +import org.jspecify.annotations.Nullable; import org.springframework.data.expression.ValueEvaluationContextProvider; import org.springframework.data.expression.ValueExpression; -import org.springframework.data.expression.ValueExpressionParser; -import org.springframework.data.mapping.model.SpELExpressionEvaluator; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind; import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind; +import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove; import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.aggregation.AggregationOperation; @@ -47,17 +45,10 @@ import org.springframework.data.mongodb.util.json.ParameterBindingContext; import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; import org.springframework.data.repository.query.ParameterAccessor; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; -import org.springframework.data.spel.ExpressionDependencies; import org.springframework.data.util.Lazy; -import org.springframework.expression.EvaluationContext; -import org.springframework.expression.ExpressionParser; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -79,41 +70,12 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { private final MongoOperations operations; private final ExecutableFind executableFind; private final ExecutableUpdate executableUpdate; + private final ExecutableRemove executableRemove; private final Lazy codec = Lazy .of(() -> new ParameterBindingDocumentCodec(getCodecRegistry())); private final ValueExpressionDelegate valueExpressionDelegate; private final ValueEvaluationContextProvider valueEvaluationContextProvider; - /** - * Creates a new {@link AbstractMongoQuery} from the given {@link MongoQueryMethod} and {@link MongoOperations}. - * - * @param method must not be {@literal null}. - * @param operations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @deprecated use the constructor version with {@link ValueExpressionDelegate} - */ - @Deprecated(since = "4.4.0") - public AbstractMongoQuery(MongoQueryMethod method, MongoOperations operations, ExpressionParser expressionParser, - QueryMethodEvaluationContextProvider evaluationContextProvider) { - - Assert.notNull(operations, "MongoOperations must not be null"); - Assert.notNull(method, "MongoQueryMethod must not be null"); - Assert.notNull(expressionParser, "SpelExpressionParser must not be null"); - Assert.notNull(evaluationContextProvider, "QueryMethodEvaluationContextProvider must not be null"); - - this.method = method; - this.operations = operations; - - MongoEntityMetadata metadata = method.getEntityInformation(); - Class type = metadata.getCollectionEntity().getType(); - - this.executableFind = operations.query(type); - this.executableUpdate = operations.update(type); - this.valueExpressionDelegate = new ValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), evaluationContextProvider.getEvaluationContextProvider()), ValueExpressionParser.create(() -> expressionParser)); - this.valueEvaluationContextProvider = valueExpressionDelegate.createValueContextProvider(method.getParameters()); - } - /** * Creates a new {@link AbstractMongoQuery} from the given {@link MongoQueryMethod} and {@link MongoOperations}. * @@ -135,6 +97,7 @@ public AbstractMongoQuery(MongoQueryMethod method, MongoOperations operations, V this.executableFind = operations.query(type); this.executableUpdate = operations.update(type); + this.executableRemove = operations.remove(type); this.valueExpressionDelegate = delegate; this.valueEvaluationContextProvider = delegate.createValueContextProvider(method.getParameters()); } @@ -145,7 +108,7 @@ public MongoQueryMethod getQueryMethod() { } @Override - public Object execute(Object[] parameters) { + public @Nullable Object execute(Object[] parameters) { ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(operations.getConverter(), new MongoParametersParameterAccessor(method, parameters)); @@ -165,8 +128,7 @@ public Object execute(Object[] parameters) { * @param accessor for providing invocation arguments. Never {@literal null}. * @param typeToRead the desired component target type. Can be {@literal null}. */ - @Nullable - protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor, + protected @Nullable Object doExecute(MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor, @Nullable Class typeToRead) { Query query = createQuery(accessor); @@ -185,7 +147,8 @@ protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, C } /** - * If present apply the {@link com.mongodb.ReadPreference} from the {@link org.springframework.data.mongodb.repository.ReadPreference} annotation. + * If present apply the {@link com.mongodb.ReadPreference} from the + * {@link org.springframework.data.mongodb.repository.ReadPreference} annotation. * * @param query must not be {@literal null}. * @return never {@literal null}. @@ -200,10 +163,11 @@ private Query applyAnnotatedReadPreferenceIfPresent(Query query) { return query.withReadPreference(com.mongodb.ReadPreference.valueOf(method.getAnnotatedReadPreference())); } - private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, FindWithQuery operation) { + @SuppressWarnings("NullAway") + MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, FindWithQuery operation) { if (isDeleteQuery()) { - return new DeleteExecution(operations, method); + return new DeleteExecution<>(executableRemove, method); } if (method.isModifyingQuery()) { @@ -320,6 +284,7 @@ protected Query createCountQuery(ConvertingParameterAccessor accessor) { * @throws IllegalStateException if no update could be found. * @since 3.4 */ + @SuppressWarnings("NullAway") protected UpdateDefinition createUpdate(ConvertingParameterAccessor accessor) { if (accessor.getUpdate() != null) { @@ -380,7 +345,7 @@ private Document bindParameters(String source, ConvertingParameterAccessor acces * @return never {@literal null}. * @since 3.4 */ - protected ParameterBindingContext prepareBindingContext(String source, ConvertingParameterAccessor accessor) { + protected ParameterBindingContext prepareBindingContext(String source, MongoParameterAccessor accessor) { ValueExpressionEvaluator evaluator = getExpressionEvaluatorFor(accessor); return new ParameterBindingContext(accessor::getBindableValue, evaluator); @@ -396,20 +361,6 @@ protected ParameterBindingDocumentCodec getParameterBindingCodec() { return codec.get(); } - /** - * Obtain a the {@link EvaluationContext} suitable to evaluate expressions backed by the given dependencies. - * - * @param dependencies must not be {@literal null}. - * @param accessor must not be {@literal null}. - * @return the {@link SpELExpressionEvaluator}. - * @since 2.4 - */ - protected SpELExpressionEvaluator getSpELExpressionEvaluatorFor(ExpressionDependencies dependencies, - ConvertingParameterAccessor accessor) { - - return new DefaultSpELExpressionEvaluator(new SpelExpressionParser(), valueEvaluationContextProvider.getEvaluationContext(accessor.getValues(), dependencies).getEvaluationContext()); - } - /** * Obtain a {@link ValueExpressionEvaluator} suitable to evaluate expressions. * @@ -418,14 +369,16 @@ protected SpELExpressionEvaluator getSpELExpressionEvaluatorFor(ExpressionDepend * @since 4.4.0 */ protected ValueExpressionEvaluator getExpressionEvaluatorFor(MongoParameterAccessor accessor) { - return new ValueExpressionDelegateValueExpressionEvaluator(valueExpressionDelegate, (ValueExpression expression) -> - valueEvaluationContextProvider.getEvaluationContext(accessor.getValues(), expression.getExpressionDependencies())); + return new ValueExpressionDelegateValueExpressionEvaluator(valueExpressionDelegate, + (ValueExpression expression) -> valueEvaluationContextProvider.getEvaluationContext(accessor.getValues(), + expression.getExpressionDependencies())); } /** * @return the {@link CodecRegistry} used. * @since 2.4 */ + @SuppressWarnings("NullAway") protected CodecRegistry getCodecRegistry() { return operations.execute(MongoDatabase::getCodecRegistry); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java index a5754a4e46..d363c93442 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java @@ -24,17 +24,15 @@ import org.bson.Document; import org.bson.codecs.configuration.CodecRegistry; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.core.convert.converter.Converter; -import org.springframework.core.env.StandardEnvironment; import org.springframework.data.expression.ReactiveValueEvaluationContextProvider; import org.springframework.data.expression.ValueEvaluationContext; import org.springframework.data.expression.ValueEvaluationContextProvider; import org.springframework.data.expression.ValueExpression; -import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.mapping.model.EntityInstantiators; -import org.springframework.data.mapping.model.SpELExpressionEvaluator; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.ReactiveFindOperation.FindWithProjection; @@ -56,15 +54,10 @@ import org.springframework.data.mongodb.util.json.ParameterBindingContext; import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; import org.springframework.data.repository.query.ParameterAccessor; -import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.spel.ExpressionDependencies; -import org.springframework.expression.ExpressionParser; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -89,45 +82,6 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery { private final ValueExpressionDelegate valueExpressionDelegate; private final ReactiveValueEvaluationContextProvider valueEvaluationContextProvider; - /** - * Creates a new {@link AbstractReactiveMongoQuery} from the given {@link MongoQueryMethod} and - * {@link MongoOperations}. - * - * @param method must not be {@literal null}. - * @param operations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @deprecated use the constructor version with {@link ValueExpressionDelegate} - */ - @Deprecated(since = "4.4.0") - public AbstractReactiveMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations operations, - ExpressionParser expressionParser, ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { - - Assert.notNull(method, "MongoQueryMethod must not be null"); - Assert.notNull(operations, "ReactiveMongoOperations must not be null"); - Assert.notNull(expressionParser, "SpelExpressionParser must not be null"); - Assert.notNull(evaluationContextProvider, "ReactiveEvaluationContextExtension must not be null"); - - this.method = method; - this.operations = operations; - this.instantiators = new EntityInstantiators(); - this.valueExpressionDelegate = new ValueExpressionDelegate( - new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), - evaluationContextProvider.getEvaluationContextProvider()), - ValueExpressionParser.create(() -> expressionParser)); - - MongoEntityMetadata metadata = method.getEntityInformation(); - Class type = metadata.getCollectionEntity().getType(); - - this.findOperationWithProjection = operations.query(type); - this.updateOps = operations.update(type); - ValueEvaluationContextProvider valueContextProvider = valueExpressionDelegate - .createValueContextProvider(method.getParameters()); - Assert.isInstanceOf(ReactiveValueEvaluationContextProvider.class, valueContextProvider, - "ValueEvaluationContextProvider must be reactive"); - this.valueEvaluationContextProvider = (ReactiveValueEvaluationContextProvider) valueContextProvider; - } - /** * Creates a new {@link AbstractReactiveMongoQuery} from the given {@link MongoQueryMethod} and * {@link MongoOperations}. @@ -241,6 +195,7 @@ private ReactiveMongoQueryExecution getExecution(MongoParameterAccessor accessor return new ResultProcessingExecution(getExecutionToWrap(accessor, operation), resultProcessing); } + @SuppressWarnings("NullAway") private ReactiveMongoQueryExecution getExecutionToWrap(MongoParameterAccessor accessor, FindWithQuery operation) { if (isDeleteQuery()) { @@ -380,6 +335,7 @@ protected Mono createCountQuery(ConvertingParameterAccessor accessor) { * @throws IllegalStateException if no update could be found. * @since 3.4 */ + @SuppressWarnings("NullAway") protected Mono createUpdate(MongoParameterAccessor accessor) { if (accessor.getUpdate() != null) { @@ -460,26 +416,6 @@ protected Mono getParameterBindingCodec() { return getCodecRegistry().map(ParameterBindingDocumentCodec::new); } - /** - * Obtain a {@link Mono publisher} emitting the {@link SpELExpressionEvaluator} suitable to evaluate expressions - * backed by the given dependencies. - * - * @param dependencies must not be {@literal null}. - * @param accessor must not be {@literal null}. - * @return a {@link Mono} emitting the {@link SpELExpressionEvaluator} when ready. - * @since 3.4 - * @deprecated since 4.4.0, use - * {@link #getValueExpressionEvaluatorLater(ExpressionDependencies, MongoParameterAccessor)} instead - */ - @Deprecated(since = "4.4.0") - protected Mono getSpelEvaluatorFor(ExpressionDependencies dependencies, - MongoParameterAccessor accessor) { - return valueEvaluationContextProvider.getEvaluationContextLater(accessor.getValues(), dependencies) - .map(evaluationContext -> (SpELExpressionEvaluator) new DefaultSpELExpressionEvaluator( - new SpelExpressionParser(), evaluationContext.getEvaluationContext())) - .defaultIfEmpty(DefaultSpELExpressionEvaluator.unsupported()); - } - /** * Obtain a {@link ValueExpressionEvaluator} suitable to evaluate expressions. * @@ -491,7 +427,7 @@ ValueExpressionEvaluator getValueExpressionEvaluator(MongoParameterAccessor acce return new ValueExpressionEvaluator() { @Override - public T evaluate(String expressionString) { + public @Nullable T evaluate(String expressionString) { ValueExpression expression = valueExpressionDelegate.parse(expressionString); ValueEvaluationContext evaluationContext = valueEvaluationContextProvider .getEvaluationContext(accessor.getValues(), expression.getExpressionDependencies()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java index 6eb6a5da89..639c694ef9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java @@ -22,7 +22,7 @@ import java.util.function.LongUnaryOperator; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort.Order; import org.springframework.data.mapping.model.ValueExpressionEvaluator; @@ -41,7 +41,6 @@ import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.util.ReflectionUtils; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -166,8 +165,7 @@ static AggregationOptions computeOptions(MongoQueryMethod method, ConvertingPara * Prepares the AggregationPipeline including type discovery and calling {@link AggregationCallback} to run the * aggregation. */ - @Nullable - static T doAggregate(AggregationPipeline pipeline, MongoQueryMethod method, ResultProcessor processor, + static @Nullable T doAggregate(AggregationPipeline pipeline, MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor, Function evaluatorFunction, AggregationCallback callback) { @@ -308,8 +306,7 @@ static void appendLimitAndOffsetIfPresent(AggregationPipeline aggregationPipelin * @return can be {@literal null} if source {@link Document#isEmpty() is empty}. * @throws IllegalArgumentException when none of the above rules is met. */ - @Nullable - static T extractSimpleTypeResult(@Nullable Document source, Class targetType, MongoConverter converter) { + static @Nullable T extractSimpleTypeResult(@Nullable Document source, Class targetType, MongoConverter converter) { if (ObjectUtils.isEmpty(source)) { return null; @@ -336,9 +333,8 @@ static T extractSimpleTypeResult(@Nullable Document source, Class targetT String.format("o_O no entry of type %s found in %s.", targetType.getSimpleName(), source.toJson())); } - @Nullable @SuppressWarnings("unchecked") - private static T getPotentiallyConvertedSimpleTypeValue(MongoConverter converter, @Nullable Object value, + private static @Nullable T getPotentiallyConvertedSimpleTypeValue(MongoConverter converter, @Nullable Object value, Class targetType) { if (value == null) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/CollationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/CollationUtils.java index 2aac6b77a8..108c6ee796 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/CollationUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/CollationUtils.java @@ -20,12 +20,12 @@ import java.util.regex.Pattern; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.util.json.ParameterBindingContext; import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; -import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.NumberUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -55,8 +55,7 @@ private CollationUtils() { * @return can be {@literal null} if neither {@link ConvertingParameterAccessor#getCollation()} nor * {@literal collationExpression} are present. */ - @Nullable - static Collation computeCollation(@Nullable String collationExpression, ConvertingParameterAccessor accessor, + static @Nullable Collation computeCollation(@Nullable String collationExpression, ConvertingParameterAccessor accessor, ValueExpressionEvaluator expressionEvaluator) { if (accessor.getCollation() != null) { @@ -98,6 +97,7 @@ static Collation computeCollation(@Nullable String collationExpression, Converti ObjectUtils.nullSafeClassName(placeholderValue))); } + Assert.notNull(placeholderValue, "PlaceholderValue must not be null"); return Collation.parse(collationExpression.replace(placeholder, placeholderValue.toString())); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java index dbf87f2f2e..f203b67e67 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java @@ -21,11 +21,15 @@ import java.util.Iterator; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.data.domain.Limit; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; +import org.springframework.data.domain.Score; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Vector; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.convert.MongoWriter; @@ -35,7 +39,6 @@ import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -74,7 +77,12 @@ public PotentiallyConvertingIterator iterator() { } @Override - public ScrollPosition getScrollPosition() { + public @Nullable Vector getVector() { + return delegate.getVector(); + } + + @Override + public @Nullable ScrollPosition getScrollPosition() { return delegate.getScrollPosition(); } @@ -87,34 +95,44 @@ public Sort getSort() { } @Override - public Class findDynamicProjection() { + public @Nullable Class findDynamicProjection() { return delegate.findDynamicProjection(); } - public Object getBindableValue(int index) { + public @Nullable Object getBindableValue(int index) { return getConvertedValue(delegate.getBindableValue(index), null); } @Override - public Range getDistanceRange() { + public @Nullable Score getScore() { + return delegate.getScore(); + } + + @Override + public @Nullable Range getScoreRange() { + return delegate.getScoreRange(); + } + + @Override + public @Nullable Range getDistanceRange() { return delegate.getDistanceRange(); } - public Point getGeoNearLocation() { + public @Nullable Point getGeoNearLocation() { return delegate.getGeoNearLocation(); } - public TextCriteria getFullText() { + public @Nullable TextCriteria getFullText() { return delegate.getFullText(); } @Override - public Collation getCollation() { + public @Nullable Collation getCollation() { return delegate.getCollation(); } @Override - public UpdateDefinition getUpdate() { + public @Nullable UpdateDefinition getUpdate() { return delegate.getUpdate(); } @@ -130,8 +148,7 @@ public Limit getLimit() { * @param typeInformation can be {@literal null}. * @return can be {@literal null}. */ - @Nullable - private Object getConvertedValue(Object value, @Nullable TypeInformation typeInformation) { + private @Nullable Object getConvertedValue(@Nullable Object value, @Nullable TypeInformation typeInformation) { return writer.convertToMongoType(value, typeInformation == null ? null : typeInformation.getActualType()); } @@ -161,11 +178,11 @@ public boolean hasNext() { return delegate.hasNext(); } - public Object next() { + public @Nullable Object next() { return delegate.next(); } - public Object nextConverted(MongoPersistentProperty property) { + public @Nullable Object nextConverted(MongoPersistentProperty property) { Object next = next(); @@ -209,7 +226,7 @@ private static Collection asCollection(@Nullable Object source) { if (source instanceof Iterable iterable) { - if(source instanceof Collection collection) { + if (source instanceof Collection collection) { return new ArrayList<>(collection); } @@ -228,7 +245,7 @@ private static Collection asCollection(@Nullable Object source) { } @Override - public Object[] getValues() { + public Object @Nullable[] getValues() { return delegate.getValues(); } @@ -244,6 +261,6 @@ public interface PotentiallyConvertingIterator extends Iterator { * * @return */ - Object nextConverted(MongoPersistentProperty property); + @Nullable Object nextConverted(MongoPersistentProperty property); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/DefaultSpELExpressionEvaluator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/DefaultSpELExpressionEvaluator.java deleted file mode 100644 index 16a1e55226..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/DefaultSpELExpressionEvaluator.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2020-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.repository.query; - -import org.springframework.data.mapping.model.SpELExpressionEvaluator; -import org.springframework.expression.EvaluationContext; -import org.springframework.expression.ExpressionParser; - -/** - * Simple {@link SpELExpressionEvaluator} implementation using {@link ExpressionParser} and {@link EvaluationContext}. - * - * @author Mark Paluch - * @since 3.1 - */ -class DefaultSpELExpressionEvaluator implements SpELExpressionEvaluator { - - private final ExpressionParser parser; - private final EvaluationContext context; - - DefaultSpELExpressionEvaluator(ExpressionParser parser, EvaluationContext context) { - this.parser = parser; - this.context = context; - } - - /** - * Return a {@link SpELExpressionEvaluator} that does not support expression evaluation. - * - * @return a {@link SpELExpressionEvaluator} that does not support expression evaluation. - * @since 3.1 - */ - public static SpELExpressionEvaluator unsupported() { - return NoOpExpressionEvaluator.INSTANCE; - } - - @Override - @SuppressWarnings("unchecked") - public T evaluate(String expression) { - return (T) parser.parseExpression(expression).getValue(context, Object.class); - } - - /** - * {@link SpELExpressionEvaluator} that does not support SpEL evaluation. - * - * @author Mark Paluch - * @since 3.1 - */ - enum NoOpExpressionEvaluator implements SpELExpressionEvaluator { - - INSTANCE; - - @Override - public T evaluate(String expression) { - throw new UnsupportedOperationException("Expression evaluation not supported"); - } - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java index 8678e5a74c..c54d689b52 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java @@ -15,9 +15,9 @@ */ package org.springframework.data.mongodb.repository.query; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.repository.core.EntityInformation; -import org.springframework.lang.Nullable; /** * Mongo specific {@link EntityInformation}. @@ -58,8 +58,7 @@ default boolean isVersioned() { * @return can be {@literal null}. * @since 2.2 */ - @Nullable - default Object getVersion(T entity) { + default @Nullable Object getVersion(T entity) { return null; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java index 5db853e810..1b52233eac 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java @@ -15,6 +15,8 @@ */ package org.springframework.data.mongodb.repository.query; +import org.jspecify.annotations.Nullable; + import org.springframework.data.domain.Range; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; @@ -23,7 +25,6 @@ import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.repository.query.ParameterAccessor; -import org.springframework.lang.Nullable; /** * Mongo-specific {@link ParameterAccessor} exposing a maximum distance parameter. @@ -41,7 +42,7 @@ public interface MongoParameterAccessor extends ParameterAccessor { * @return the maximum distance to apply to the geo query or {@literal null} if there's no {@link Distance} parameter * at all or the given value for it was {@literal null}. */ - Range getDistanceRange(); + @Nullable Range getDistanceRange(); /** * Returns the {@link Point} to use for a geo-near query. @@ -75,7 +76,7 @@ public interface MongoParameterAccessor extends ParameterAccessor { * @return * @since 1.8 */ - Object[] getValues(); + Object @Nullable[] getValues(); /** * Returns the {@link Update} to be used for an update execution. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java index 1f66d5b77d..94acef17ce 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java @@ -20,8 +20,11 @@ import java.util.Arrays; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; import org.springframework.data.domain.Range; +import org.springframework.data.domain.Vector; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoPage; import org.springframework.data.geo.GeoResult; @@ -36,7 +39,6 @@ import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersSource; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; /** * Custom extension of {@link Parameters} discovering additional @@ -53,9 +55,9 @@ public class MongoParameters extends Parameters private final int rangeIndex; private final int maxDistanceIndex; - private final @Nullable Integer fullTextIndex; - private final @Nullable Integer nearIndex; - private final @Nullable Integer collationIndex; + private final int fullTextIndex; + private final int nearIndex; + private final int collationIndex; private final int updateIndex; private final TypeInformation domainType; @@ -106,9 +108,8 @@ private MongoParameters(ParametersSource parametersSource, NearIndex nearIndex) this.nearIndex = nearIndex.nearIndex; } - private MongoParameters(List parameters, int maxDistanceIndex, @Nullable Integer nearIndex, - @Nullable Integer fullTextIndex, int rangeIndex, @Nullable Integer collationIndex, int updateIndex, - TypeInformation domainType) { + private MongoParameters(List parameters, int maxDistanceIndex, int nearIndex, int fullTextIndex, + int rangeIndex, int collationIndex, int updateIndex, TypeInformation domainType) { super(parameters); @@ -141,7 +142,7 @@ static boolean isGeoNearQuery(Method method) { static class NearIndex { - private final @Nullable Integer nearIndex; + private final int nearIndex; public NearIndex(ParametersSource parametersSource, boolean isGeoNearMethod) { @@ -196,10 +197,6 @@ static int findNearIndexInParameters(Method method) { return index; } - public int getDistanceRangeIndex() { - return -1; - } - /** * Returns the index of the {@link Distance} parameter to be used for max distance in geo queries. * @@ -226,7 +223,7 @@ public int getNearIndex() { * @since 1.6 */ public int getFullTextParameterIndex() { - return fullTextIndex != null ? fullTextIndex : -1; + return fullTextIndex; } /** @@ -234,7 +231,7 @@ public int getFullTextParameterIndex() { * @since 1.6 */ public boolean hasFullTextParameter() { - return this.fullTextIndex != null && this.fullTextIndex >= 0; + return this.fullTextIndex >= 0; } /** @@ -252,7 +249,7 @@ public int getRangeIndex() { * @since 2.2 */ public int getCollationParameterIndex() { - return collationIndex != null ? collationIndex : -1; + return collationIndex; } /** @@ -318,7 +315,8 @@ static class MongoParameter extends Parameter { @Override public boolean isSpecialParameter() { - return super.isSpecialParameter() || Distance.class.isAssignableFrom(getType()) || isNearParameter() + return super.isSpecialParameter() || Distance.class.isAssignableFrom(getType()) + || Vector.class.isAssignableFrom(getType()) || isNearParameter() || TextCriteria.class.isAssignableFrom(getType()) || Collation.class.isAssignableFrom(getType()); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java index ac1931e10c..0f56223492 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java @@ -15,8 +15,11 @@ */ package org.springframework.data.mongodb.repository.query; +import org.jspecify.annotations.Nullable; + import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; +import org.springframework.data.domain.Score; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.query.Collation; @@ -24,7 +27,7 @@ import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -53,6 +56,24 @@ public MongoParametersParameterAccessor(MongoQueryMethod method, Object[] values this.method = method; } + @SuppressWarnings("NullAway") + @Override + public Range getScoreRange() { + + MongoParameters mongoParameters = method.getParameters(); + + if (mongoParameters.hasScoreRangeParameter()) { + return getValue(mongoParameters.getScoreRangeIndex()); + } + + Score score = getScore(); + Bound maxDistance = score != null ? Bound.inclusive(score) : Bound.unbounded(); + + return Range.of(Bound.unbounded(), maxDistance); + } + + @SuppressWarnings("NullAway") + @Override public Range getDistanceRange() { MongoParameters mongoParameters = method.getParameters(); @@ -70,7 +91,7 @@ public Range getDistanceRange() { return Range.of(Bound.unbounded(), maxDistance); } - public Point getGeoNearLocation() { + public @Nullable Point getGeoNearLocation() { int nearIndex = method.getParameters().getNearIndex(); @@ -95,14 +116,14 @@ public Point getGeoNearLocation() { return (Point) value; } - @Nullable @Override - public TextCriteria getFullText() { + public @Nullable TextCriteria getFullText() { int index = method.getParameters().getFullTextParameterIndex(); return index >= 0 ? potentiallyConvertFullText(getValue(index)) : null; } - protected TextCriteria potentiallyConvertFullText(Object fullText) { + @Contract("null -> fail") + protected TextCriteria potentiallyConvertFullText(@Nullable Object fullText) { Assert.notNull(fullText, "Fulltext parameter must not be 'null'."); @@ -124,7 +145,7 @@ protected TextCriteria potentiallyConvertFullText(Object fullText) { } @Override - public Collation getCollation() { + public @Nullable Collation getCollation() { if (method.getParameters().getCollationParameterIndex() == -1) { return null; @@ -134,12 +155,12 @@ public Collation getCollation() { } @Override - public Object[] getValues() { + public Object @Nullable[] getValues() { return super.getValues(); } @Override - public UpdateDefinition getUpdate() { + public @Nullable UpdateDefinition getUpdate() { int updateIndex = method.getParameters().getUpdateIndex(); return updateIndex == -1 ? null : (UpdateDefinition) getValue(updateIndex); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java index 66a8870623..ba7394ec17 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java @@ -15,7 +15,8 @@ */ package org.springframework.data.mongodb.repository.query; -import static org.springframework.data.mongodb.core.query.Criteria.*; +import static org.springframework.data.mongodb.core.query.Criteria.Placeholder; +import static org.springframework.data.mongodb.core.query.Criteria.where; import java.util.Arrays; import java.util.Collection; @@ -26,6 +27,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.bson.BsonRegularExpression; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; import org.springframework.data.domain.Sort; @@ -52,7 +54,6 @@ import org.springframework.data.repository.query.parser.Part.Type; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.data.util.Streamable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -65,13 +66,14 @@ * @author Christoph Strobl * @author Edward Prentice */ -class MongoQueryCreator extends AbstractQueryCreator { +public class MongoQueryCreator extends AbstractQueryCreator { private static final Log LOG = LogFactory.getLog(MongoQueryCreator.class); private final MongoParameterAccessor accessor; private final MappingContext context; private final boolean isGeoNearQuery; + private final boolean isSearchQuery; /** * Creates a new {@link MongoQueryCreator} from the given {@link PartTree}, {@link ConvertingParameterAccessor} and @@ -81,9 +83,9 @@ class MongoQueryCreator extends AbstractQueryCreator { * @param accessor * @param context */ - public MongoQueryCreator(PartTree tree, ConvertingParameterAccessor accessor, + public MongoQueryCreator(PartTree tree, MongoParameterAccessor accessor, MappingContext context) { - this(tree, accessor, context, false); + this(tree, accessor, context, false, false); } /** @@ -94,9 +96,10 @@ public MongoQueryCreator(PartTree tree, ConvertingParameterAccessor accessor, * @param accessor * @param context * @param isGeoNearQuery + * @param isSearchQuery */ - public MongoQueryCreator(PartTree tree, ConvertingParameterAccessor accessor, - MappingContext context, boolean isGeoNearQuery) { + public MongoQueryCreator(PartTree tree, MongoParameterAccessor accessor, + MappingContext context, boolean isGeoNearQuery, boolean isSearchQuery) { super(tree, accessor); @@ -104,6 +107,7 @@ public MongoQueryCreator(PartTree tree, ConvertingParameterAccessor accessor, this.accessor = accessor; this.isGeoNearQuery = isGeoNearQuery; + this.isSearchQuery = isSearchQuery; this.context = context; } @@ -111,7 +115,12 @@ public MongoQueryCreator(PartTree tree, ConvertingParameterAccessor accessor, protected Criteria create(Part part, Iterator iterator) { if (isGeoNearQuery && part.getType().equals(Type.NEAR)) { - return null; + return new Criteria(); + } + + if (isPartOfSearchQuery(part)) { + skip(part, iterator); + return new Criteria(); } PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); @@ -127,6 +136,11 @@ protected Criteria and(Part part, Criteria base, Iterator iterator) { return create(part, iterator); } + if (isPartOfSearchQuery(part)) { + skip(part, iterator); + return base; + } + PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); MongoPersistentProperty property = path.getLeafProperty(); @@ -141,7 +155,7 @@ protected Criteria or(Criteria base, Criteria criteria) { } @Override - protected Query complete(Criteria criteria, Sort sort) { + protected Query complete(@Nullable Criteria criteria, Sort sort) { Query query = (criteria == null ? new Query() : new Query(criteria)).with(sort); @@ -161,6 +175,7 @@ protected Query complete(Criteria criteria, Sort sort) { * @param parameters * @return */ + @SuppressWarnings("NullAway") private Criteria from(Part part, MongoPersistentProperty property, Criteria criteria, Iterator parameters) { Type type = part.getType(); @@ -183,9 +198,17 @@ private Criteria from(Part part, MongoPersistentProperty property, Criteria crit case IS_NULL: return criteria.is(null); case NOT_IN: - return criteria.nin(nextAsList(parameters, part)); + Object ninValue = parameters.next(); + if (ninValue instanceof Placeholder) { + return criteria.raw("$nin", ninValue); + } + return criteria.nin(valueAsList(ninValue, part)); case IN: - return criteria.in(nextAsList(parameters, part)); + Object inValue = parameters.next(); + if (inValue instanceof Placeholder) { + return criteria.raw("$in", inValue); + } + return criteria.in(valueAsList(inValue, part)); case LIKE: case STARTING_WITH: case ENDING_WITH: @@ -200,7 +223,12 @@ private Criteria from(Part part, MongoPersistentProperty property, Criteria crit Object param = parameters.next(); return param instanceof Pattern pattern ? criteria.regex(pattern) : criteria.regex(param.toString()); case EXISTS: - return criteria.exists((Boolean) parameters.next()); + Object next = parameters.next(); + if (next instanceof Placeholder placeholder) { + return criteria.raw("$exists", placeholder); + } else { + return criteria.exists((Boolean) next); + } case TRUE: return criteria.is(true); case FALSE: @@ -319,7 +347,11 @@ private Criteria createContainingCriteria(Part part, MongoPersistentProperty pro Iterator parameters) { if (property.isCollectionLike()) { - return criteria.in(nextAsList(parameters, part)); + Object next = parameters.next(); + if (next instanceof Placeholder) { + return criteria.raw("$in", next); + } + return criteria.in(valueAsList(next, part)); } return addAppropriateLikeRegexTo(criteria, part, parameters.next()); @@ -333,6 +365,7 @@ private Criteria createContainingCriteria(Part part, MongoPersistentProperty pro * @param value * @return the criteria extended with the regex. */ + @SuppressWarnings("NullAway") private Criteria addAppropriateLikeRegexTo(Criteria criteria, Part part, Object value) { if (value == null) { @@ -348,8 +381,7 @@ private Criteria addAppropriateLikeRegexTo(Criteria criteria, Part part, Object * @param part * @return the regex options or {@literal null}. */ - @Nullable - private String toRegexOptions(Part part) { + private @Nullable String toRegexOptions(Part part) { String regexOptions = null; switch (part.shouldIgnoreCase()) { @@ -383,19 +415,18 @@ private T nextAs(Iterator iterator, Class type) { String.format("Expected parameter type of %s but got %s", type, parameter.getClass())); } - private java.util.List nextAsList(Iterator iterator, Part part) { + private java.util.List valueAsList(Object value, Part part) { - Streamable streamable = asStreamable(iterator.next()); + Streamable streamable = asStreamable(value); if (!isSimpleComparisonPossible(part)) { MatchMode matchMode = toMatchMode(part.getType()); String regexOptions = toRegexOptions(part); streamable = streamable.map(it -> { - if (it instanceof String value) { + if (it instanceof String sv) { - return new BsonRegularExpression(MongoRegexCreator.INSTANCE.toRegularExpression(value, matchMode), - regexOptions); + return new BsonRegularExpression(MongoRegexCreator.INSTANCE.toRegularExpression(sv, matchMode), regexOptions); } return it; }); @@ -414,10 +445,11 @@ private Streamable asStreamable(Object value) { return Streamable.of(value); } - private String toLikeRegex(String source, Part part) { + private @Nullable String toLikeRegex(String source, Part part) { return MongoRegexCreator.INSTANCE.toRegularExpression(source, toMatchMode(part.getType())); } + @SuppressWarnings("NullAway") private boolean isSpherical(MongoPersistentProperty property) { if (property.isAnnotationPresent(GeoSpatialIndexed.class)) { @@ -428,10 +460,23 @@ private boolean isSpherical(MongoPersistentProperty property) { return false; } + private boolean isPartOfSearchQuery(Part part) { + return isSearchQuery && (part.getType().equals(Type.NEAR) || part.getType().equals(Type.WITHIN)); + } + + private static void skip(Part part, Iterator parameters) { + + int total = part.getNumberOfArguments(); + int i = 0; + while (parameters.hasNext() && i < total) { + parameters.next(); + i++; + } + } + /** * Compute a {@link Type#BETWEEN} typed {@link Part} using {@link Criteria#gt(Object) $gt}, - * {@link Criteria#gte(Object) $gte}, {@link Criteria#lt(Object) $lt} and {@link Criteria#lte(Object) $lte}. - *
+ * {@link Criteria#gte(Object) $gte}, {@link Criteria#lt(Object) $lt} and {@link Criteria#lte(Object) $lte}.
* In case the first {@literal value} is actually a {@link Range} the lower and upper bounds of the {@link Range} are * used according to their {@link Bound#isInclusive() inclusion} definition. Otherwise the {@literal value} is used * for {@literal $gt} and {@link Iterator#next() parameters.next()} as {@literal $lt}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java index dd2b78de59..c0531e0e19 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java @@ -15,12 +15,19 @@ */ package org.springframework.data.mongodb.repository.query; +import java.util.Collection; +import java.util.Iterator; import java.util.List; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; +import org.springframework.data.domain.ScoringFunction; +import org.springframework.data.domain.SearchResult; +import org.springframework.data.domain.SearchResults; +import org.springframework.data.domain.Similarity; import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; import org.springframework.data.geo.Distance; @@ -28,18 +35,26 @@ import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Point; +import org.springframework.data.mongodb.core.ExecutableAggregationOperation.TerminatingAggregation; import org.springframework.data.mongodb.core.ExecutableFindOperation; import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind; +import org.springframework.data.mongodb.core.ExecutableRemoveOperation; +import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove; +import org.springframework.data.mongodb.core.ExecutableRemoveOperation.TerminatingRemove; import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate; import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.aggregation.AggregationPipeline; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.core.aggregation.TypedAggregation; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; +import org.springframework.data.mongodb.repository.query.VectorSearchDelegate.QueryContainer; import org.springframework.data.mongodb.repository.util.SliceUtils; +import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -55,7 +70,7 @@ * @author Christoph Strobl */ @FunctionalInterface -interface MongoQueryExecution { +public interface MongoQueryExecution { @Nullable Object execute(Query query); @@ -67,12 +82,12 @@ interface MongoQueryExecution { * @author Christoph Strobl * @since 1.5 */ - final class SlicedExecution implements MongoQueryExecution { + final class SlicedExecution implements MongoQueryExecution { - private final FindWithQuery find; + private final FindWithQuery find; private final Pageable pageable; - public SlicedExecution(ExecutableFindOperation.FindWithQuery find, Pageable pageable) { + public SlicedExecution(ExecutableFindOperation.FindWithQuery find, Pageable pageable) { Assert.notNull(find, "Find must not be null"); Assert.notNull(pageable, "Pageable must not be null"); @@ -83,7 +98,7 @@ public SlicedExecution(ExecutableFindOperation.FindWithQuery find, Pageable p @Override @SuppressWarnings({ "unchecked", "rawtypes" }) - public Object execute(Query query) { + public Slice execute(Query query) { int pageSize = pageable.getPageSize(); @@ -93,7 +108,7 @@ public Object execute(Query query) { boolean hasNext = result.size() > pageSize; - return new SliceImpl(hasNext ? result.subList(0, pageSize) : result, pageable, hasNext); + return new SliceImpl(hasNext ? result.subList(0, pageSize) : result, pageable, hasNext); } } @@ -104,12 +119,12 @@ public Object execute(Query query) { * @author Mark Paluch * @author Christoph Strobl */ - final class PagedExecution implements MongoQueryExecution { + final class PagedExecution implements MongoQueryExecution { - private final FindWithQuery operation; + private final FindWithQuery operation; private final Pageable pageable; - public PagedExecution(ExecutableFindOperation.FindWithQuery operation, Pageable pageable) { + public PagedExecution(ExecutableFindOperation.FindWithQuery operation, Pageable pageable) { Assert.notNull(operation, "Operation must not be null"); Assert.notNull(pageable, "Pageable must not be null"); @@ -119,11 +134,11 @@ public PagedExecution(ExecutableFindOperation.FindWithQuery operation, Pageab } @Override - public Object execute(Query query) { + public Page execute(Query query) { int overallLimit = query.getLimit(); - TerminatingFind matching = operation.matching(query); + TerminatingFind matching = operation.matching(query); // Apply raw pagination query.with(pageable); @@ -171,10 +186,12 @@ public Object execute(Query query) { return isListOfGeoResult(method.getReturnType()) ? results.getContent() : results; } - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "NullAway" }) GeoResults doExecuteQuery(Query query) { Point nearLocation = accessor.getGeoNearLocation(); + Assert.notNull(nearLocation, "[query.location] must not be null"); + NearQuery nearQuery = NearQuery.near(nearLocation); if (query != null) { @@ -182,6 +199,8 @@ GeoResults doExecuteQuery(Query query) { } Range distances = accessor.getDistanceRange(); + Assert.notNull(nearLocation, "[query.distance] must not be null"); + distances.getLowerBound().getValue().ifPresent(it -> nearQuery.minDistance(it).in(it.getMetric())); distances.getUpperBound().getValue().ifPresent(it -> nearQuery.maxDistance(it).in(it.getMetric())); @@ -202,6 +221,84 @@ private static boolean isListOfGeoResult(TypeInformation returnType) { } } + /** + * {@link MongoQueryExecution} to execute vector search. + * + * @author Mark Paluch + * @author Chistoph Strobl + * @since 5.0 + */ + class VectorSearchExecution implements MongoQueryExecution { + + private final MongoOperations operations; + private final TypeInformation returnType; + private final String collectionName; + private final Class targetType; + private final ScoringFunction scoringFunction; + private final AggregationPipeline pipeline; + + VectorSearchExecution(MongoOperations operations, MongoQueryMethod method, String collectionName, + QueryContainer queryContainer) { + this(operations, queryContainer.outputType(), collectionName, method.getReturnType(), queryContainer.pipeline(), + queryContainer.scoringFunction()); + } + + public VectorSearchExecution(MongoOperations operations, Class targetType, String collectionName, + TypeInformation returnType, AggregationPipeline pipeline, ScoringFunction scoringFunction) { + + this.operations = operations; + this.returnType = returnType; + this.collectionName = collectionName; + this.targetType = targetType; + this.scoringFunction = scoringFunction; + this.pipeline = pipeline; + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Object execute(Query query) { + + TerminatingAggregation executableAggregation = operations.aggregateAndReturn(targetType) + .inCollection(collectionName).by(TypedAggregation.newAggregation(targetType, pipeline.getOperations())); + + if (!isSearchResult(returnType)) { + return executableAggregation.all().getMappedResults(); + } + + AggregationResults> result = executableAggregation + .map((raw, container) -> new SearchResult<>(container.get(), + Similarity.raw(raw.getDouble("__score__"), scoringFunction))) + .all(); + + return isListOfSearchResult(returnType) ? result.getMappedResults() + : new SearchResults(result.getMappedResults()); + } + + private static boolean isListOfSearchResult(TypeInformation returnType) { + + if (!Collection.class.isAssignableFrom(returnType.getType())) { + return false; + } + + TypeInformation componentType = returnType.getComponentType(); + return componentType != null && SearchResult.class.equals(componentType.getType()); + } + + private static boolean isSearchResult(TypeInformation returnType) { + + if (SearchResults.class.isAssignableFrom(returnType.getType())) { + return true; + } + + if (!Iterable.class.isAssignableFrom(returnType.getType())) { + return false; + } + + TypeInformation componentType = returnType.getComponentType(); + return componentType != null && SearchResult.class.equals(componentType.getType()); + } + } + /** * {@link MongoQueryExecution} to execute geo-near queries with paging. * @@ -252,36 +349,46 @@ public Object execute(Query query) { * @author Christoph Strobl * @since 1.5 */ - final class DeleteExecution implements MongoQueryExecution { - - private final MongoOperations operations; - private final MongoQueryMethod method; - - public DeleteExecution(MongoOperations operations, MongoQueryMethod method) { - - Assert.notNull(operations, "Operations must not be null"); - Assert.notNull(method, "Method must not be null"); + final class DeleteExecution implements MongoQueryExecution { + + private ExecutableRemoveOperation.ExecutableRemove remove; + private Type type; + + public DeleteExecution(ExecutableRemove remove, QueryMethod queryMethod) { + this.remove = remove; + if (queryMethod.isCollectionQuery()) { + this.type = Type.FIND_AND_REMOVE_ALL; + } else if (queryMethod.isQueryForEntity() + && !ClassUtils.isPrimitiveOrWrapper(queryMethod.getReturnedObjectType())) { + this.type = Type.FIND_AND_REMOVE_ONE; + } else { + this.type = Type.ALL; + } + } - this.operations = operations; - this.method = method; + public DeleteExecution(ExecutableRemove remove, Type type) { + this.remove = remove; + this.type = type; } @Override - public Object execute(Query query) { - - String collectionName = method.getEntityInformation().getCollectionName(); - Class type = method.getEntityInformation().getJavaType(); - - if (method.isCollectionQuery()) { - return operations.findAllAndRemove(query, type, collectionName); - } - - if (method.isQueryForEntity() && !ClassUtils.isPrimitiveOrWrapper(method.getReturnedObjectType())) { - return operations.findAndRemove(query, type, collectionName); + public @Nullable Object execute(Query query) { + + TerminatingRemove doRemove = remove.matching(query); + if (Type.ALL.equals(type)) { + DeleteResult result = doRemove.all(); + return result.wasAcknowledged() ? Long.valueOf(result.getDeletedCount()) : Long.valueOf(0); + } else if (Type.FIND_AND_REMOVE_ALL.equals(type)) { + return doRemove.findAndRemove(); + } else if (Type.FIND_AND_REMOVE_ONE.equals(type)) { + Iterator removed = doRemove.findAndRemove().iterator(); + return removed.hasNext() ? removed.next() : null; } + throw new RuntimeException(); + } - DeleteResult writeResult = operations.remove(query, type, collectionName); - return writeResult.wasAcknowledged() ? writeResult.getDeletedCount() : 0L; + public enum Type { + FIND_AND_REMOVE_ONE, FIND_AND_REMOVE_ALL, ALL } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java index d3fe22b4ef..de628d59f4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java @@ -21,6 +21,7 @@ import java.util.Optional; import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.annotation.Collation; @@ -34,6 +35,7 @@ import org.springframework.data.mongodb.repository.ReadPreference; import org.springframework.data.mongodb.repository.Tailable; import org.springframework.data.mongodb.repository.Update; +import org.springframework.data.mongodb.repository.VectorSearch; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; @@ -43,7 +45,6 @@ import org.springframework.data.util.ReactiveWrappers; import org.springframework.data.util.ReflectionUtils; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; @@ -117,7 +118,7 @@ public boolean hasAnnotatedQuery() { * @return */ @Nullable - String getAnnotatedQuery() { + public String getAnnotatedQuery() { return findAnnotatedQuery().orElse(null); } @@ -191,7 +192,7 @@ public boolean isGeoNearQuery() { } /** - * Returns the {@link Query} annotation that is applied to the method or {@code null} if none available. + * Returns the {@link Query} annotation that is applied to the method or {@literal null} if none available. * * @return */ @@ -204,7 +205,7 @@ Optional lookupQueryAnnotation() { return doFindAnnotation(Query.class); } - TypeInformation getReturnType() { + public TypeInformation getReturnType() { return TypeInformation.fromReturnTypeOf(method); } @@ -217,7 +218,7 @@ public boolean hasQueryMetaAttributes() { } /** - * Returns the {@link Meta} annotation that is applied to the method or {@code null} if not available. + * Returns the {@link Meta} annotation that is applied to the method or {@literal null} if not available. * * @return * @since 1.6 @@ -228,7 +229,7 @@ Meta getMetaAnnotation() { } /** - * Returns the {@link Tailable} annotation that is applied to the method or {@code null} if not available. + * Returns the {@link Tailable} annotation that is applied to the method or {@literal null} if not available. * * @return * @since 2.0 @@ -414,10 +415,28 @@ private Optional findAnnotatedAggregation() { .filter(it -> !ObjectUtils.isEmpty(it)); } + /** + * Returns whether the method has an annotated vector search. + * + * @return true if {@link VectorSearch} is present. + * @since 5.0 + */ + public boolean hasAnnotatedVectorSearch() { + return findAnnotatedVectorSearch().isPresent(); + } + + Optional findAnnotatedVectorSearch() { + return lookupVectorSearchAnnotation(); + } + Optional lookupAggregationAnnotation() { return doFindAnnotation(Aggregation.class); } + Optional lookupVectorSearchAnnotation() { + return doFindAnnotation(VectorSearch.class); + } + Optional lookupUpdateAnnotation() { return doFindAnnotation(Update.class); } @@ -461,7 +480,7 @@ public boolean hasAnnotatedUpdate() { * @return the {@link Update} or {@literal null} if not present. * @since 3.4 */ - public Update getUpdateSource() { + public @Nullable Update getUpdateSource() { return lookupUpdateAnnotation().orElse(null); } @@ -471,6 +490,7 @@ public Update getUpdateSource() { * @since 3.4 * @throws IllegalStateException */ + @SuppressWarnings("NullAway") public void verify() { if (isModifyingQuery()) { @@ -509,6 +529,7 @@ public void verify() { } } + @SuppressWarnings("NullAway") private boolean isNumericOrVoidReturnValue() { Class resultType = getReturnedObjectType(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java index afabf9c37e..9682e4971f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java @@ -18,8 +18,6 @@ import org.bson.Document; import org.bson.json.JsonParseException; -import org.springframework.core.env.StandardEnvironment; -import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.MongoTemplate; @@ -29,14 +27,11 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.data.repository.query.QueryMethod; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.RepositoryQuery; 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.PartTree; -import org.springframework.expression.ExpressionParser; import org.springframework.util.StringUtils; /** @@ -54,26 +49,6 @@ public class PartTreeMongoQuery extends AbstractMongoQuery { private final MappingContext context; private final ResultProcessor processor; - /** - * Creates a new {@link PartTreeMongoQuery} from the given {@link QueryMethod} and {@link MongoTemplate}. - * - * @param method must not be {@literal null}. - * @param mongoOperations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @deprecated since 4.4, use the constructors accepting {@link QueryMethodValueEvaluationContextAccessor} instead. - */ - @Deprecated(since = "4.4.0") - public PartTreeMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations, ExpressionParser expressionParser, - QueryMethodEvaluationContextProvider evaluationContextProvider) { - super(method, mongoOperations, expressionParser, evaluationContextProvider); - - this.processor = method.getResultProcessor(); - this.tree = new PartTree(method.getName(), processor.getReturnedType().getDomainType()); - this.isGeoNearQuery = method.isGeoNearQuery(); - this.context = mongoOperations.getConverter().getMappingContext(); - } - /** * Creates a new {@link PartTreeMongoQuery} from the given {@link QueryMethod} and {@link MongoTemplate}. * @@ -103,9 +78,10 @@ public PartTree getTree() { } @Override + @SuppressWarnings("NullAway") protected Query createQuery(ConvertingParameterAccessor accessor) { - MongoQueryCreator creator = new MongoQueryCreator(tree, accessor, context, isGeoNearQuery); + MongoQueryCreator creator = new MongoQueryCreator(tree, accessor, context, isGeoNearQuery, false); Query query = creator.createQuery(); if (tree.isLimiting()) { @@ -150,7 +126,7 @@ protected Query createQuery(ConvertingParameterAccessor accessor) { @Override protected Query createCountQuery(ConvertingParameterAccessor accessor) { - return new MongoQueryCreator(tree, accessor, context, false).createQuery(); + return new MongoQueryCreator(tree, accessor, context, false, false).createQuery(); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java index 431510f11b..4b7262749a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java @@ -21,13 +21,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.ProxyFactory; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mapping.model.ValueExpressionEvaluator; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoParameterAccessor.java index 324f01d61f..9534a9cf4f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoParameterAccessor.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.repository.query; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -51,16 +52,21 @@ public ReactiveMongoParameterAccessor(MongoQueryMethod method, Object[] values) * @see org.springframework.data.mongodb.repository.query.MongoParametersParameterAccessor#getValues() */ @Override - public Object[] getValues() { + public Object @Nullable[] getValues() { - Object[] result = new Object[super.getValues().length]; + Object[] values = super.getValues(); + if(values == null) { + return new Object[0]; + } + + Object[] result = new Object[values.length]; for (int i = 0; i < result.length; i++) { result[i] = getValue(i); } return result; } - public Object getBindableValue(int index) { + public @Nullable Object getBindableValue(int index) { return getValue(getParameters().getBindableParameter(index).getIndex()); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java index d18c6a989c..29e2127e18 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java @@ -18,26 +18,32 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.DtoInstantiatingConverter; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; +import org.springframework.data.domain.SearchResult; +import org.springframework.data.domain.Similarity; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.Point; import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.mongodb.core.ReactiveMongoOperations; import org.springframework.data.mongodb.core.ReactiveUpdateOperation.ReactiveUpdate; +import org.springframework.data.mongodb.core.aggregation.AggregationPipeline; +import org.springframework.data.mongodb.core.aggregation.TypedAggregation; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; +import org.springframework.data.mongodb.repository.query.VectorSearchDelegate.QueryContainer; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.util.ReactiveWrappers; import org.springframework.data.util.ReflectionUtils; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -86,6 +92,8 @@ public Publisher execute(Query query, Class type, String co private Flux> doExecuteQuery(@Nullable Query query, Class type, String collection) { Point nearLocation = accessor.getGeoNearLocation(); + Assert.notNull(nearLocation, "[query.location] ist not present"); + NearQuery nearQuery = NearQuery.near(nearLocation); if (query != null) { @@ -93,6 +101,8 @@ private Flux> doExecuteQuery(@Nullable Query query, Class t } Range distances = accessor.getDistanceRange(); + + Assert.notNull(distances, "[query.range] ist not present"); distances.getUpperBound().getValue().ifPresent(it -> nearQuery.maxDistance(it).in(it.getMetric())); distances.getLowerBound().getValue().ifPresent(it -> nearQuery.minDistance(it).in(it.getMetric())); @@ -113,6 +123,57 @@ private boolean isStreamOfGeoResult() { } } + /** + * {@link ReactiveMongoQueryExecution} to execute vector search. + * + * @author Mark Paluch + * @since 5.0 + */ + class VectorSearchExecution implements ReactiveMongoQueryExecution { + + private final ReactiveMongoOperations operations; + private final QueryContainer queryMetadata; + private final AggregationPipeline pipeline; + private final boolean returnSearchResult; + + VectorSearchExecution(ReactiveMongoOperations operations, MongoQueryMethod method, QueryContainer queryMetadata) { + + this.operations = operations; + this.queryMetadata = queryMetadata; + this.pipeline = queryMetadata.pipeline(); + this.returnSearchResult = isSearchResult(method.getReturnType()); + } + + @Override + public Publisher execute(Query query, Class type, String collection) { + + Flux aggregate = operations.aggregate( + TypedAggregation.newAggregation(queryMetadata.outputType(), pipeline.getOperations()), collection, + Document.class); + + return aggregate.map(document -> { + + Object mappedResult = operations.getConverter().read(queryMetadata.outputType(), document); + + return returnSearchResult + ? new SearchResult<>(mappedResult, + Similarity.raw(document.getDouble(queryMetadata.scoreField()), queryMetadata.scoringFunction())) + : mappedResult; + }); + } + + private static boolean isSearchResult(TypeInformation returnType) { + + if (!Publisher.class.isAssignableFrom(returnType.getType())) { + return false; + } + + TypeInformation componentType = returnType.getComponentType(); + return componentType != null && SearchResult.class.equals(componentType.getType()); + } + + } + /** * {@link ReactiveMongoQueryExecution} removing documents matching the query. * @@ -195,6 +256,7 @@ public ResultProcessingExecution(ReactiveMongoQueryExecution delegate, Converter } @Override + @SuppressWarnings("NullAway") public Publisher execute(Query query, Class type, String collection) { return (Publisher) converter.convert(delegate.execute(query, type, collection)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java index 5787cca5a5..9a17b2b5fc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java @@ -28,14 +28,11 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.data.repository.query.QueryMethod; -import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; 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.PartTree; -import org.springframework.expression.ExpressionParser; import org.springframework.util.StringUtils; /** @@ -52,26 +49,6 @@ public class ReactivePartTreeMongoQuery extends AbstractReactiveMongoQuery { private final MappingContext context; private final ResultProcessor processor; - /** - * Creates a new {@link ReactivePartTreeMongoQuery} from the given {@link QueryMethod} and {@link MongoTemplate}. - * - * @param method must not be {@literal null}. - * @param mongoOperations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @deprecated since 4.4.0, use the constructors accepting {@link QueryMethodValueEvaluationContextAccessor} instead. - */ - @Deprecated(since = "4.4.0") - public ReactivePartTreeMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations, - ExpressionParser expressionParser, ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { - super(method, mongoOperations, expressionParser, evaluationContextProvider); - - this.processor = method.getResultProcessor(); - this.tree = new PartTree(method.getName(), processor.getReturnedType().getDomainType()); - this.isGeoNearQuery = method.isGeoNearQuery(); - this.context = mongoOperations.getConverter().getMappingContext(); - } - /** * Creates a new {@link ReactivePartTreeMongoQuery} from the given {@link QueryMethod} and {@link MongoTemplate}. * @@ -110,9 +87,10 @@ protected Mono createCountQuery(ConvertingParameterAccessor accessor) { return Mono.fromSupplier(() -> createQueryInternal(accessor, true)); } + @SuppressWarnings("NullAway") private Query createQueryInternal(ConvertingParameterAccessor accessor, boolean isCountQuery) { - MongoQueryCreator creator = new MongoQueryCreator(tree, accessor, context, !isCountQuery && isGeoNearQuery); + MongoQueryCreator creator = new MongoQueryCreator(tree, accessor, context, !isCountQuery && isGeoNearQuery, false); Query query = creator.createQuery(); if (isCountQuery) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java index ff01d8f8a3..ebc33cef96 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java @@ -21,6 +21,7 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.data.mongodb.core.ReactiveMongoOperations; @@ -29,12 +30,9 @@ import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.util.ReflectionUtils; -import org.springframework.expression.ExpressionParser; -import org.springframework.lang.Nullable; /** * A reactive {@link org.springframework.data.repository.query.RepositoryQuery} to use a plain JSON String to create an @@ -49,24 +47,6 @@ public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery { private final ReactiveMongoOperations reactiveMongoOperations; private final MongoConverter mongoConverter; - /** - * @param method must not be {@literal null}. - * @param reactiveMongoOperations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @deprecated since 4.4.0, use the constructors accepting {@link ValueExpressionDelegate} instead. - */ - @Deprecated(since = "4.4.0") - public ReactiveStringBasedAggregation(ReactiveMongoQueryMethod method, - ReactiveMongoOperations reactiveMongoOperations, ExpressionParser expressionParser, - ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { - - super(method, reactiveMongoOperations, expressionParser, evaluationContextProvider); - - this.reactiveMongoOperations = reactiveMongoOperations; - this.mongoConverter = reactiveMongoOperations.getConverter(); - } - /** * @param method must not be {@literal null}. * @param reactiveMongoOperations must not be {@literal null}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java index 0e980fcfaf..4bfe2ca39f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java @@ -15,12 +15,14 @@ */ package org.springframework.data.mongodb.repository.query; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import reactor.core.publisher.Mono; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.bson.Document; - +import org.jspecify.annotations.NonNull; import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.ReactiveMongoOperations; @@ -28,13 +30,8 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.util.json.ParameterBindingContext; import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; -import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.spel.ExpressionDependencies; -import org.springframework.expression.ExpressionParser; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.NonNull; import org.springframework.util.Assert; /** @@ -50,7 +47,7 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery { private static final Log LOG = LogFactory.getLog(ReactiveStringBasedMongoQuery.class); private final String query; - private final String fieldSpec; + private final @Nullable String fieldSpec; private final ValueExpressionParser expressionParser; @@ -59,73 +56,15 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery { private final boolean isDeleteQuery; /** - * Creates a new {@link ReactiveStringBasedMongoQuery} for the given {@link MongoQueryMethod} and - * {@link MongoOperations}. - * - * @param method must not be {@literal null}. - * @param mongoOperations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @deprecated since 4.4.0, use the constructors accepting {@link ValueExpressionDelegate} instead. - */ - @Deprecated(since = "4.4.0") - public ReactiveStringBasedMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations, - ExpressionParser expressionParser, ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { - this(method.getAnnotatedQuery(), method, mongoOperations, expressionParser, evaluationContextProvider); - } - - /** - * Creates a new {@link ReactiveStringBasedMongoQuery} for the given {@link String}, {@link MongoQueryMethod}, - * {@link MongoOperations}, {@link SpelExpressionParser} and - * {@link ReactiveExtensionAwareQueryMethodEvaluationContextProvider}. - * - * @param query must not be {@literal null}. - * @param method must not be {@literal null}. - * @param mongoOperations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @deprecated since 4.4.0, use the constructors accepting {@link ValueExpressionDelegate} instead. - */ - @Deprecated(since = "4.4.0") - public ReactiveStringBasedMongoQuery(String query, ReactiveMongoQueryMethod method, - ReactiveMongoOperations mongoOperations, ExpressionParser expressionParser, - ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { - super(method, mongoOperations, expressionParser, evaluationContextProvider); - - Assert.notNull(query, "Query must not be null"); - - this.query = query; - this.expressionParser = ValueExpressionParser.create(() -> expressionParser); - this.fieldSpec = method.getFieldSpecification(); - - if (method.hasAnnotatedQuery()) { - - org.springframework.data.mongodb.repository.Query queryAnnotation = method.getQueryAnnotation(); - - this.isCountQuery = queryAnnotation.count(); - this.isExistsQuery = queryAnnotation.exists(); - this.isDeleteQuery = queryAnnotation.delete(); - - if (hasAmbiguousProjectionFlags(this.isCountQuery, this.isExistsQuery, this.isDeleteQuery)) { - throw new IllegalArgumentException(String.format(COUNT_EXISTS_AND_DELETE, method)); - } - - } else { - - this.isCountQuery = false; - this.isExistsQuery = false; - this.isDeleteQuery = false; - } - } - - /** - * Creates a new {@link ReactiveStringBasedMongoQuery} for the given {@link MongoQueryMethod}, - * {@link MongoOperations} and {@link ValueExpressionDelegate}. + * Creates a new {@link ReactiveStringBasedMongoQuery} for the given {@link MongoQueryMethod}, {@link MongoOperations} + * and {@link ValueExpressionDelegate}. * * @param method must not be {@literal null}. * @param mongoOperations must not be {@literal null}. * @param delegate must not be {@literal null}. * @since 4.4.0 */ + @SuppressWarnings("NullAway") public ReactiveStringBasedMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations, ValueExpressionDelegate delegate) { this(method.getAnnotatedQuery(), method, mongoOperations, delegate); @@ -141,7 +80,8 @@ public ReactiveStringBasedMongoQuery(ReactiveMongoQueryMethod method, ReactiveMo * @param delegate must not be {@literal null}. * @since 4.4.0 */ - public ReactiveStringBasedMongoQuery(@NonNull String query, ReactiveMongoQueryMethod method, + @SuppressWarnings("NullAway") + public ReactiveStringBasedMongoQuery(String query, ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations, ValueExpressionDelegate delegate) { super(method, mongoOperations, delegate); @@ -195,7 +135,7 @@ protected Mono createQuery(ConvertingParameterAccessor accessor) { }); } - private Mono getBindingContext(String json, ConvertingParameterAccessor accessor, + private Mono getBindingContext(@Nullable String json, ConvertingParameterAccessor accessor, ParameterBindingDocumentCodec codec) { ExpressionDependencies dependencies = codec.captureExpressionDependencies(json, accessor::getBindableValue, diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveVectorSearchAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveVectorSearchAggregation.java new file mode 100644 index 0000000000..cf75c7db94 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveVectorSearchAggregation.java @@ -0,0 +1,123 @@ +/* + * 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.mongodb.repository.query; + +import reactor.core.publisher.Mono; + +import org.bson.Document; +import org.reactivestreams.Publisher; +import org.springframework.data.mongodb.InvalidMongoDbApiUsageException; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.repository.VectorSearch; +import org.springframework.data.mongodb.repository.query.VectorSearchDelegate.QueryContainer; +import org.springframework.data.mongodb.util.json.ParameterBindingContext; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.data.spel.ExpressionDependencies; + +/** + * {@link AbstractReactiveMongoQuery} implementation to run a {@link VectorSearchAggregation}. The pre-filter is either + * derived from the method name or provided through {@link VectorSearch#filter()}. + * + * @author Mark Paluch + * @since 5.0 + */ +public class ReactiveVectorSearchAggregation extends AbstractReactiveMongoQuery { + + private final ReactiveMongoOperations mongoOperations; + private final MongoPersistentEntity collectionEntity; + private final ValueExpressionDelegate valueExpressionDelegate; + private final VectorSearchDelegate delegate; + + /** + * Creates a new {@link ReactiveVectorSearchAggregation} from the given {@link MongoQueryMethod} and + * {@link MongoOperations}. + * + * @param method must not be {@literal null}. + * @param mongoOperations must not be {@literal null}. + * @param delegate must not be {@literal null}. + */ + public ReactiveVectorSearchAggregation(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations, + ValueExpressionDelegate delegate) { + + super(method, mongoOperations, delegate); + + this.valueExpressionDelegate = delegate; + if (!method.isSearchQuery() && !method.isCollectionQuery()) { + throw new InvalidMongoDbApiUsageException(String.format( + "Repository Vector Search method '%s' must return either return SearchResults or List but was %s", + method.getName(), method.getReturnType().getType().getSimpleName())); + } + + this.mongoOperations = mongoOperations; + this.collectionEntity = method.getEntityInformation().getCollectionEntity(); + this.delegate = new VectorSearchDelegate(method, mongoOperations.getConverter(), delegate); + } + + @Override + protected Publisher doExecute(ReactiveMongoQueryMethod method, ResultProcessor processor, + ConvertingParameterAccessor accessor, @org.jspecify.annotations.Nullable Class typeToRead) { + + return getParameterBindingCodec().flatMapMany(codec -> { + + String json = delegate.getQueryString(); + ExpressionDependencies dependencies = codec.captureExpressionDependencies(json, accessor::getBindableValue, + valueExpressionDelegate); + + return getValueExpressionEvaluatorLater(dependencies, accessor).flatMapMany(expressionEvaluator -> { + + ParameterBindingContext bindingContext = new ParameterBindingContext(accessor::getBindableValue, + expressionEvaluator); + QueryContainer query = delegate.createQuery(expressionEvaluator, processor, accessor, typeToRead, codec, + bindingContext); + + ReactiveMongoQueryExecution.VectorSearchExecution execution = new ReactiveMongoQueryExecution.VectorSearchExecution( + mongoOperations, method, query); + + return execution.execute(query.query(), Document.class, collectionEntity.getCollection()); + }); + }); + } + + @Override + protected Mono createQuery(ConvertingParameterAccessor accessor) { + throw new UnsupportedOperationException(); + } + + @Override + protected boolean isCountQuery() { + return false; + } + + @Override + protected boolean isExistsQuery() { + return false; + } + + @Override + protected boolean isDeleteQuery() { + return false; + } + + @Override + protected boolean isLimiting() { + return false; + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringAggregationOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringAggregationOperation.java index 724c8f29ef..289b953b27 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringAggregationOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringAggregationOperation.java @@ -20,9 +20,9 @@ import java.util.regex.Pattern; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.AggregationOperation; import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext; -import org.springframework.lang.Nullable; /** * String-based aggregation operation for a repository query method. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java index 7ad5d78fa6..3f6a48e84c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java @@ -20,7 +20,7 @@ import java.util.stream.Stream; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.SliceImpl; import org.springframework.data.mongodb.InvalidMongoDbApiUsageException; @@ -29,13 +29,9 @@ import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.util.ReflectionUtils; -import org.springframework.expression.ExpressionParser; -import org.springframework.lang.Nullable; /** * {@link AbstractMongoQuery} implementation to run string-based aggregations using @@ -51,30 +47,6 @@ public class StringBasedAggregation extends AbstractMongoQuery { private final MongoOperations mongoOperations; private final MongoConverter mongoConverter; - /** - * Creates a new {@link StringBasedAggregation} from the given {@link MongoQueryMethod} and {@link MongoOperations}. - * - * @param method must not be {@literal null}. - * @param mongoOperations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @deprecated since 4.4.0, use the constructors accepting {@link QueryMethodValueEvaluationContextAccessor} instead. - */ - @Deprecated(since = "4.4.0") - public StringBasedAggregation(MongoQueryMethod method, MongoOperations mongoOperations, - ExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { - super(method, mongoOperations, expressionParser, evaluationContextProvider); - - if (method.isPageQuery()) { - throw new InvalidMongoDbApiUsageException(String.format( - "Repository aggregation method '%s' does not support '%s' return type; Please use 'Slice' or 'List' instead", - method.getName(), method.getReturnType().getType().getSimpleName())); - } - - this.mongoOperations = mongoOperations; - this.mongoConverter = mongoOperations.getConverter(); - } - /** * Creates a new {@link StringBasedAggregation} from the given {@link MongoQueryMethod} and {@link MongoOperations}. * @@ -99,8 +71,7 @@ public StringBasedAggregation(MongoQueryMethod method, MongoOperations mongoOper @SuppressWarnings("unchecked") @Override - @Nullable - protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor, + protected @Nullable Object doExecute(MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor, @Nullable Class ignore) { return AggregationUtils.doAggregate(AggregationUtils.computePipeline(this, method, accessor), method, processor, diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java index abc158f88a..c990d3269d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java @@ -22,11 +22,8 @@ import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.ValueExpressionDelegate; -import org.springframework.expression.ExpressionParser; -import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; /** @@ -49,47 +46,6 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { private final boolean isExistsQuery; private final boolean isDeleteQuery; - /** - * Creates a new {@link StringBasedMongoQuery} for the given {@link MongoQueryMethod}, {@link MongoOperations}, - * {@link SpelExpressionParser} and {@link QueryMethodEvaluationContextProvider}. - * - * @param method must not be {@literal null}. - * @param mongoOperations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @deprecated since 4.4.0, use the constructors accepting {@link ValueExpressionDelegate} instead. - */ - @Deprecated(since = "4.4.0") - public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations, - ExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { - super(method, mongoOperations, expressionParser, evaluationContextProvider); - - String query = method.getAnnotatedQuery(); - Assert.notNull(query, "Query must not be null"); - - this.query = query; - this.fieldSpec = method.getFieldSpecification(); - - if (method.hasAnnotatedQuery()) { - - org.springframework.data.mongodb.repository.Query queryAnnotation = method.getQueryAnnotation(); - - this.isCountQuery = queryAnnotation.count(); - this.isExistsQuery = queryAnnotation.exists(); - this.isDeleteQuery = queryAnnotation.delete(); - - if (hasAmbiguousProjectionFlags(this.isCountQuery, this.isExistsQuery, this.isDeleteQuery)) { - throw new IllegalArgumentException(String.format(COUNT_EXISTS_AND_DELETE, method)); - } - - } else { - - this.isCountQuery = false; - this.isExistsQuery = false; - this.isDeleteQuery = false; - } - } - /** * Creates a new {@link StringBasedMongoQuery} for the given {@link MongoQueryMethod}, {@link MongoOperations}, * {@link ValueExpressionDelegate}. @@ -99,6 +55,7 @@ public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOpera * @param expressionSupport must not be {@literal null}. * @since 4.4.0 */ + @SuppressWarnings("NullAway") public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations, ValueExpressionDelegate expressionSupport) { this(method.getAnnotatedQuery(), method, mongoOperations, expressionSupport); @@ -114,6 +71,7 @@ public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOpera * @param expressionSupport must not be {@literal null}. * @since 4.3 */ + @SuppressWarnings("NullAway") public StringBasedMongoQuery(String query, MongoQueryMethod method, MongoOperations mongoOperations, ValueExpressionDelegate expressionSupport) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ValueExpressionDelegateValueExpressionEvaluator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ValueExpressionDelegateValueExpressionEvaluator.java index c479f3faa9..360f5e80eb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ValueExpressionDelegateValueExpressionEvaluator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ValueExpressionDelegateValueExpressionEvaluator.java @@ -17,6 +17,7 @@ import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.springframework.data.expression.ValueEvaluationContext; import org.springframework.data.expression.ValueExpression; import org.springframework.data.mapping.model.ValueExpressionEvaluator; @@ -34,7 +35,7 @@ class ValueExpressionDelegateValueExpressionEvaluator implements ValueExpression @SuppressWarnings("unchecked") @Override - public T evaluate(String expressionString) { + public @Nullable T evaluate(String expressionString) { ValueExpression expression = delegate.parse(expressionString); return (T) expression.evaluate(expressionToContext.apply(expression)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregation.java new file mode 100644 index 0000000000..eb8dc2e52e --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregation.java @@ -0,0 +1,112 @@ +/* + * 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.mongodb.repository.query; + +import org.jspecify.annotations.Nullable; +import org.springframework.data.mapping.model.ValueExpressionEvaluator; +import org.springframework.data.mongodb.InvalidMongoDbApiUsageException; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.repository.VectorSearch; +import org.springframework.data.mongodb.repository.query.VectorSearchDelegate.QueryContainer; +import org.springframework.data.mongodb.util.json.ParameterBindingContext; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ValueExpressionDelegate; + +/** + * {@link AbstractMongoQuery} implementation to run a {@link VectorSearchAggregation}. The pre-filter is either derived + * from the method name or provided through {@link VectorSearch#filter()}. + * + * @author Mark Paluch + * @since 5.0 + */ +public class VectorSearchAggregation extends AbstractMongoQuery { + + private final MongoOperations mongoOperations; + private final MongoPersistentEntity collectionEntity; + private final VectorSearchDelegate delegate; + + /** + * Creates a new {@link VectorSearchAggregation} from the given {@link MongoQueryMethod} and {@link MongoOperations}. + * + * @param method must not be {@literal null}. + * @param mongoOperations must not be {@literal null}. + * @param delegate must not be {@literal null}. + */ + public VectorSearchAggregation(MongoQueryMethod method, MongoOperations mongoOperations, + ValueExpressionDelegate delegate) { + + super(method, mongoOperations, delegate); + + if (!method.isSearchQuery() && !method.isCollectionQuery()) { + throw new InvalidMongoDbApiUsageException(String.format( + "Repository Vector Search method '%s' must return either return SearchResults or List but was %s", + method.getName(), method.getReturnType().getType().getSimpleName())); + } + + this.mongoOperations = mongoOperations; + this.collectionEntity = method.getEntityInformation().getCollectionEntity(); + this.delegate = new VectorSearchDelegate(method, mongoOperations.getConverter(), delegate); + } + + @Override + protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor, + @Nullable Class typeToRead) { + + QueryContainer query = createVectorSearchQuery(processor, accessor, typeToRead); + + MongoQueryExecution.VectorSearchExecution execution = new MongoQueryExecution.VectorSearchExecution(mongoOperations, + method, collectionEntity.getCollection(), query); + + return execution.execute(query.query()); + } + + QueryContainer createVectorSearchQuery(ResultProcessor processor, MongoParameterAccessor accessor, + @Nullable Class typeToRead) { + + ValueExpressionEvaluator evaluator = getExpressionEvaluatorFor(accessor); + ParameterBindingContext bindingContext = prepareBindingContext(delegate.getQueryString(), accessor); + + return delegate.createQuery(evaluator, processor, accessor, typeToRead, getParameterBindingCodec(), bindingContext); + } + + @Override + protected Query createQuery(ConvertingParameterAccessor accessor) { + throw new UnsupportedOperationException(); + } + + @Override + protected boolean isCountQuery() { + return false; + } + + @Override + protected boolean isExistsQuery() { + return false; + } + + @Override + protected boolean isDeleteQuery() { + return false; + } + + @Override + protected boolean isLimiting() { + return false; + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegate.java new file mode 100644 index 0000000000..0dbff2e932 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegate.java @@ -0,0 +1,422 @@ +/* + * 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.mongodb.repository.query; + +import java.util.ArrayList; +import java.util.List; + +import org.bson.Document; +import org.jspecify.annotations.Nullable; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.ScoringFunction; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Vector; +import org.springframework.data.expression.ValueExpression; +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.ValueExpressionEvaluator; +import org.springframework.data.mongodb.InvalidMongoDbApiUsageException; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationOperation; +import org.springframework.data.mongodb.core.aggregation.AggregationPipeline; +import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.repository.VectorSearch; +import org.springframework.data.mongodb.util.json.ParameterBindingContext; +import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.util.NumberUtils; +import org.springframework.util.StringUtils; + +/** + * Delegate to assemble information about Vector Search queries necessary to run a MongoDB {@code $vectorSearch}. + * + * @author Mark Paluch + */ +class VectorSearchDelegate { + + private final VectorSearchQueryFactory queryFactory; + private final VectorSearchOperation.SearchType searchType; + private final String indexName; + private final @Nullable Integer numCandidates; + private final @Nullable String numCandidatesExpression; + private final Limit limit; + private final @Nullable String limitExpression; + private final MongoConverter converter; + + VectorSearchDelegate(MongoQueryMethod method, MongoConverter converter, ValueExpressionDelegate delegate) { + + VectorSearch vectorSearch = method.findAnnotatedVectorSearch().orElseThrow(); + + this.searchType = vectorSearch.searchType(); + this.indexName = method.getAnnotatedHint(); + + if (StringUtils.hasText(vectorSearch.numCandidates())) { + + ValueExpression expression = delegate.getValueExpressionParser().parse(vectorSearch.numCandidates()); + + if (expression.isLiteral()) { + this.numCandidates = Integer.parseInt(vectorSearch.numCandidates()); + this.numCandidatesExpression = null; + } else { + this.numCandidates = null; + this.numCandidatesExpression = vectorSearch.numCandidates(); + } + + } else { + this.numCandidates = null; + this.numCandidatesExpression = null; + } + + if (StringUtils.hasText(vectorSearch.limit())) { + + ValueExpression expression = delegate.getValueExpressionParser().parse(vectorSearch.limit()); + + if (expression.isLiteral()) { + this.limit = Limit.of(Integer.parseInt(vectorSearch.limit())); + this.limitExpression = null; + } else { + this.limit = Limit.unlimited(); + this.limitExpression = vectorSearch.limit(); + } + + } else { + this.limit = Limit.unlimited(); + this.limitExpression = null; + } + + this.converter = converter; + + if (StringUtils.hasText(vectorSearch.filter())) { + this.queryFactory = StringUtils.hasText(vectorSearch.path()) + ? new AnnotatedQueryFactory(vectorSearch.filter(), vectorSearch.path()) + : new AnnotatedQueryFactory(vectorSearch.filter(), method.getEntityInformation().getCollectionEntity()); + } else { + this.queryFactory = new PartTreeQueryFactory( + new PartTree(method.getName(), method.getResultProcessor().getReturnedType().getDomainType()), + converter.getMappingContext()); + } + } + + /** + * Create Query Metadata for {@code $vectorSearch}. + */ + QueryContainer createQuery(ValueExpressionEvaluator evaluator, ResultProcessor processor, + MongoParameterAccessor accessor, @Nullable Class typeToRead, ParameterBindingDocumentCodec codec, + ParameterBindingContext context) { + + String scoreField = "__score__"; + Class outputType = typeToRead != null ? typeToRead : processor.getReturnedType().getReturnedType(); + VectorSearchInput vectorSearchInput = createSearchInput(evaluator, accessor, codec, context); + AggregationPipeline pipeline = createVectorSearchPipeline(vectorSearchInput, scoreField, outputType, accessor, + evaluator); + + return new QueryContainer(vectorSearchInput.path, scoreField, vectorSearchInput.query, pipeline, searchType, + outputType, getSimilarityFunction(accessor), indexName); + } + + @SuppressWarnings("NullAway") + AggregationPipeline createVectorSearchPipeline(VectorSearchInput input, String scoreField, Class outputType, + MongoParameterAccessor accessor, ValueExpressionEvaluator evaluator) { + + Vector vector = accessor.getVector(); + Score score = accessor.getScore(); + Range distance = accessor.getScoreRange(); + Limit limit = Limit.of(input.query().getLimit()); + + List stages = new ArrayList<>(); + VectorSearchOperation $vectorSearch = Aggregation.vectorSearch(indexName).path(input.path()).vector(vector) + .limit(limit); + + Integer candidates = null; + if (this.numCandidatesExpression != null) { + candidates = ((Number) evaluator.evaluate(this.numCandidatesExpression)).intValue(); + } else if (this.numCandidates != null) { + candidates = this.numCandidates; + } else if (input.query().isLimited() && (searchType == VectorSearchOperation.SearchType.ANN + || searchType == VectorSearchOperation.SearchType.DEFAULT)) { + + /* + MongoDB: We recommend that you specify a number at least 20 times higher than the number of documents to return (limit) to increase accuracy. + */ + candidates = input.query().getLimit() * 20; + } + + if (candidates != null) { + $vectorSearch = $vectorSearch.numCandidates(candidates); + } + // + $vectorSearch = $vectorSearch.filter(input.query.getQueryObject()); + $vectorSearch = $vectorSearch.searchType(this.searchType); + $vectorSearch = $vectorSearch.withSearchScore(scoreField); + + if (score != null) { + $vectorSearch = $vectorSearch.withFilterBySore(c -> { + c.gt(score.getValue()); + }); + } else if (distance.getLowerBound().isBounded() || distance.getUpperBound().isBounded()) { + $vectorSearch = $vectorSearch.withFilterBySore(c -> { + Range.Bound lower = distance.getLowerBound(); + if (lower.isBounded()) { + double value = lower.getValue().get().getValue(); + if (lower.isInclusive()) { + c.gte(value); + } else { + c.gt(value); + } + } + + Range.Bound upper = distance.getUpperBound(); + if (upper.isBounded()) { + + double value = upper.getValue().get().getValue(); + if (upper.isInclusive()) { + c.lte(value); + } else { + c.lt(value); + } + } + }); + } + + stages.add($vectorSearch); + + if (input.query().isSorted()) { + + stages.add(ctx -> { + + Document mappedSort = ctx.getMappedObject(input.query().getSortObject(), outputType); + mappedSort.append(scoreField, -1); + return ctx.getMappedObject(new Document("$sort", mappedSort)); + }); + } else { + stages.add(Aggregation.sort(Sort.Direction.DESC, scoreField)); + } + + return new AggregationPipeline(stages); + } + + private VectorSearchInput createSearchInput(ValueExpressionEvaluator evaluator, MongoParameterAccessor accessor, + ParameterBindingDocumentCodec codec, ParameterBindingContext context) { + + VectorSearchInput input = queryFactory.createQuery(accessor, codec, context); + Limit limit = getLimit(evaluator, accessor); + if(!input.query.isLimited() || (input.query.isLimited() && !limit.isUnlimited())) { + input.query().limit(limit); + } + return input; + } + + private Limit getLimit(ValueExpressionEvaluator evaluator, MongoParameterAccessor accessor) { + + if (this.limitExpression != null) { + + Object value = evaluator.evaluate(this.limitExpression); + if (value != null) { + if (value instanceof Limit l) { + return l; + } + if (value instanceof Number n) { + return Limit.of(n.intValue()); + } + if (value instanceof String s) { + return Limit.of(NumberUtils.parseNumber(s, Integer.class)); + } + throw new IllegalArgumentException("Invalid type for Limit. Found [%s], expected Limit or Number"); + } + } + + if (this.limit.isLimited()) { + return this.limit; + } + + return accessor.getLimit(); + } + + public String getQueryString() { + return queryFactory.getQueryString(); + } + + ScoringFunction getSimilarityFunction(MongoParameterAccessor accessor) { + + Score score = accessor.getScore(); + + if (score != null) { + return score.getFunction(); + } + + Range scoreRange = accessor.getScoreRange(); + + if (scoreRange != null) { + if (scoreRange.getUpperBound().isBounded()) { + return scoreRange.getUpperBound().getValue().get().getFunction(); + } + + if (scoreRange.getLowerBound().isBounded()) { + return scoreRange.getLowerBound().getValue().get().getFunction(); + } + } + + return ScoringFunction.unspecified(); + } + + /** + * Metadata for a Vector Search Aggregation. + * + * @param path + * @param query + * @param searchType + * @param outputType + * @param scoringFunction + */ + record QueryContainer(String path, String scoreField, Query query, AggregationPipeline pipeline, + VectorSearchOperation.SearchType searchType, Class outputType, ScoringFunction scoringFunction, String index) { + + } + + /** + * Strategy interface to implement a query factory for the Vector Search pre-filter query. + */ + private interface VectorSearchQueryFactory { + + VectorSearchInput createQuery(MongoParameterAccessor parameterAccessor, ParameterBindingDocumentCodec codec, + ParameterBindingContext context); + + /** + * @return the underlying query string to determine {@link ParameterBindingContext}. + */ + String getQueryString(); + } + + private static class AnnotatedQueryFactory implements VectorSearchQueryFactory { + + private final String query; + private final String path; + + AnnotatedQueryFactory(String query, String path) { + + this.query = query; + this.path = path; + } + + AnnotatedQueryFactory(String query, MongoPersistentEntity entity) { + + this.query = query; + String path = null; + for (MongoPersistentProperty property : entity) { + if (Vector.class.isAssignableFrom(property.getType())) { + path = property.getFieldName(); + break; + } + } + + if (path == null) { + throw new InvalidMongoDbApiUsageException( + "Cannot find Vector Search property in entity [%s]".formatted(entity.getName())); + } + + this.path = path; + } + + public VectorSearchInput createQuery(MongoParameterAccessor parameterAccessor, ParameterBindingDocumentCodec codec, + ParameterBindingContext context) { + + Document queryObject = codec.decode(this.query, context); + Query query = new BasicQuery(queryObject); + + Sort sort = parameterAccessor.getSort(); + if (sort.isSorted()) { + query = query.with(sort); + } + + return new VectorSearchInput(path, query); + } + + @Override + public String getQueryString() { + return this.query; + } + } + + private class PartTreeQueryFactory implements VectorSearchQueryFactory { + + private final String path; + private final PartTree tree; + + @SuppressWarnings("NullableProblems") + PartTreeQueryFactory(PartTree tree, MappingContext context) { + + String path = null; + for (PartTree.OrPart part : tree) { + for (Part p : part) { + if (p.getType() == Part.Type.SIMPLE_PROPERTY || p.getType() == Part.Type.NEAR + || p.getType() == Part.Type.WITHIN || p.getType() == Part.Type.BETWEEN) { + PersistentPropertyPath ppp = context.getPersistentPropertyPath(p.getProperty()); + MongoPersistentProperty property = ppp.getLeafProperty(); + + if (Vector.class.isAssignableFrom(property.getType())) { + path = p.getProperty().toDotPath(); + break; + } + } + } + } + + if (path == null) { + throw new InvalidMongoDbApiUsageException( + "No Simple Property/Near/Within/Between part found for a Vector property"); + } + + this.path = path; + this.tree = tree; + } + + @SuppressWarnings("NullAway") + public VectorSearchInput createQuery(MongoParameterAccessor parameterAccessor, ParameterBindingDocumentCodec codec, + ParameterBindingContext context) { + + MongoQueryCreator creator = new MongoQueryCreator(tree, parameterAccessor, converter.getMappingContext(), false, + true); + + Query query = creator.createQuery(parameterAccessor.getSort()); + + if (tree.isLimiting()) { + query.limit(tree.getMaxResults()); + } + + return new VectorSearchInput(path, query); + } + + @Override + public String getQueryString() { + return ""; + } + } + + private record VectorSearchInput(String path, Query query) { + + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/package-info.java index 20c77e22aa..5f0cc21049 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/package-info.java @@ -1,6 +1,6 @@ /** * Query derivation mechanism for MongoDB specific repositories. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.repository.query; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CrudMethodMetadataPostProcessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CrudMethodMetadataPostProcessor.java index f59a995170..037bd60672 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CrudMethodMetadataPostProcessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CrudMethodMetadataPostProcessor.java @@ -25,6 +25,7 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.aop.TargetSource; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.factory.BeanClassLoaderAware; @@ -32,13 +33,13 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import com.mongodb.ReadPreference; +import org.springframework.util.StringUtils; /** * {@link RepositoryProxyPostProcessor} that sets up interceptors to read metadata information from the invoked method. @@ -54,7 +55,7 @@ class CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor, B private @Nullable ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); @Override - public void setBeanClassLoader(ClassLoader classLoader) { + public void setBeanClassLoader(@Nullable ClassLoader classLoader) { this.classLoader = classLoader; } @@ -121,7 +122,7 @@ static MethodInvocation currentInvocation() throws IllegalStateException { } @Override - public Object invoke(MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { Method method = invocation.getMethod(); @@ -193,7 +194,7 @@ private static Optional findReadPreference(AnnotatedElement... a org.springframework.data.mongodb.repository.ReadPreference preference = AnnotatedElementUtils .findMergedAnnotation(element, org.springframework.data.mongodb.repository.ReadPreference.class); - if (preference != null) { + if (preference != null && StringUtils.hasText(preference.value())) { return Optional.of(com.mongodb.ReadPreference.valueOf(preference.value())); } } @@ -220,7 +221,7 @@ public boolean isStatic() { } @Override - public Object getTarget() { + public @Nullable Object getTarget() { MethodInvocation invocation = CrudMethodMetadataPopulatingMethodInterceptor.currentInvocation(); return TransactionSynchronizationManager.getResource(invocation.getMethod()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java index 1d876289be..443108d2f0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java @@ -16,12 +16,12 @@ package org.springframework.data.mongodb.repository.support; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; import org.springframework.data.repository.core.support.PersistentEntityInformation; -import org.springframework.lang.Nullable; /** * {@link MongoEntityInformation} implementation using a {@link MongoPersistentEntity} instance to lookup the necessary @@ -113,7 +113,7 @@ public boolean isVersioned() { } @Override - public Object getVersion(T entity) { + public @Nullable Object getVersion(T entity) { if (!isVersioned()) { return null; @@ -124,8 +124,7 @@ public Object getVersion(T entity) { return accessor.getProperty(this.entityMetadata.getRequiredVersionProperty()); } - @Nullable - public Collation getCollation() { + public @Nullable Collation getCollation() { return this.entityMetadata.getCollation(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoAnnotationProcessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoAnnotationProcessor.java index 3c029ee5aa..6deee469e1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoAnnotationProcessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoAnnotationProcessor.java @@ -22,9 +22,8 @@ import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import javax.tools.Diagnostic; - +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.Document; -import org.springframework.lang.Nullable; import com.querydsl.apt.AbstractQuerydslProcessor; import com.querydsl.apt.Configuration; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoEntityInformationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoEntityInformationSupport.java index d0a3f7a1e4..1a39198757 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoEntityInformationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoEntityInformationSupport.java @@ -15,9 +15,9 @@ */ package org.springframework.data.mongodb.repository.support; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java index baf069c3a4..a309cea0a3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java @@ -15,14 +15,12 @@ */ package org.springframework.data.mongodb.repository.support; -import static org.springframework.data.querydsl.QuerydslUtils.*; - -import java.io.Serializable; import java.lang.reflect.Method; import java.util.Optional; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanFactory; -import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; @@ -33,8 +31,8 @@ import org.springframework.data.mongodb.repository.query.PartTreeMongoQuery; import org.springframework.data.mongodb.repository.query.StringBasedAggregation; import org.springframework.data.mongodb.repository.query.StringBasedMongoQuery; +import org.springframework.data.mongodb.repository.query.VectorSearchAggregation; import org.springframework.data.projection.ProjectionFactory; -import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; @@ -42,10 +40,8 @@ import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; -import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -61,7 +57,7 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport { private final CrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor(); private final MongoOperations operations; private final MappingContext, MongoPersistentProperty> mappingContext; - @Nullable private QueryMethodValueEvaluationContextAccessor accessor; + private MongoRepositoryFragmentsContributor fragmentsContributor = MongoRepositoryFragmentsContributor.DEFAULT; /** * Creates a new {@link MongoRepositoryFactory} with the given {@link MongoOperations}. @@ -78,15 +74,26 @@ public MongoRepositoryFactory(MongoOperations mongoOperations) { addRepositoryProxyPostProcessor(crudMethodMetadataPostProcessor); } + /** + * Configures the {@link MongoRepositoryFragmentsContributor} to be used. Defaults to + * {@link MongoRepositoryFragmentsContributor#DEFAULT}. + * + * @param fragmentsContributor + * @since 5.0 + */ + public void setFragmentsContributor(MongoRepositoryFragmentsContributor fragmentsContributor) { + this.fragmentsContributor = fragmentsContributor; + } + @Override - public void setBeanClassLoader(ClassLoader classLoader) { + public void setBeanClassLoader(@Nullable ClassLoader classLoader) { super.setBeanClassLoader(classLoader); crudMethodMetadataPostProcessor.setBeanClassLoader(classLoader); } @Override - protected ProjectionFactory getProjectionFactory(ClassLoader classLoader, BeanFactory beanFactory) { + protected ProjectionFactory getProjectionFactory(@Nullable ClassLoader classLoader, @Nullable BeanFactory beanFactory) { return this.operations.getConverter().getProjectionFactory(); } @@ -101,40 +108,24 @@ protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata } /** - * Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add Mongo-specific extensions. Typically - * adds a {@link QuerydslMongoPredicateExecutor} if the repository interface uses Querydsl. + * Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add Mongo-specific extensions. + * Typically, adds a {@link QuerydslMongoPredicateExecutor} if the repository interface uses Querydsl. *

- * Can be overridden by subclasses to customize {@link RepositoryFragments}. + * Built-in fragment contribution can be customized by configuring {@link MongoRepositoryFragmentsContributor}. * * @param metadata repository metadata. * @param operations the MongoDB operations manager. - * @return + * @return {@link RepositoryFragments} to be added to the repository. * @since 3.2.1 */ protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata, MongoOperations operations) { - - boolean isQueryDslRepository = QUERY_DSL_PRESENT - && QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); - - if (isQueryDslRepository) { - - if (metadata.isReactiveRepository()) { - throw new InvalidDataAccessApiUsageException( - "Cannot combine Querydsl and reactive repository support in a single interface"); - } - - return RepositoryFragments - .just(new QuerydslMongoPredicateExecutor<>(getEntityInformation(metadata.getDomainType()), operations)); - } - - return RepositoryFragments.empty(); + return fragmentsContributor.contribute(metadata, getEntityInformation(metadata), operations); } @Override protected Object getTargetRepository(RepositoryInformation information) { - MongoEntityInformation entityInformation = getEntityInformation(information.getDomainType(), - information); + MongoEntityInformation entityInformation = getEntityInformation(information); Object targetRepository = getTargetRepositoryViaReflection(information, entityInformation, operations); if (targetRepository instanceof SimpleMongoRepository repository) { @@ -150,16 +141,18 @@ protected Optional getQueryLookupStrategy(@Nullable Key key return Optional.of(new MongoQueryLookupStrategy(operations, mappingContext, valueExpressionDelegate)); } + @Deprecated + @Override public MongoEntityInformation getEntityInformation(Class domainClass) { - return getEntityInformation(domainClass, null); + MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainClass); + return MongoEntityInformationSupport.entityInformationFor(entity, null); } - private MongoEntityInformation getEntityInformation(Class domainClass, - @Nullable RepositoryMetadata metadata) { + @Override + public MongoEntityInformation getEntityInformation(RepositoryMetadata metadata) { - MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainClass); - return MongoEntityInformationSupport. entityInformationFor(entity, - metadata != null ? metadata.getIdType() : null); + MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(metadata.getDomainType()); + return MongoEntityInformationSupport.entityInformationFor(entity, metadata.getIdType()); } /** @@ -184,6 +177,8 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, if (namedQueries.hasQuery(namedQueryName)) { String namedQuery = namedQueries.getQuery(namedQueryName); return new StringBasedMongoQuery(namedQuery, queryMethod, operations, expressionSupport); + } else if (queryMethod.hasAnnotatedVectorSearch()) { + return new VectorSearchAggregation(queryMethod, operations, expressionSupport); } else if (queryMethod.hasAnnotatedAggregation()) { return new StringBasedAggregation(queryMethod, operations, expressionSupport); } else if (queryMethod.hasAnnotatedQuery()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java index c98d38c5f5..18c7c5a13c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java @@ -17,24 +17,27 @@ import java.io.Serializable; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.core.support.RepositoryFactorySupport; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** * {@link org.springframework.beans.factory.FactoryBean} to create {@link MongoRepository} instances. * * @author Oliver Gierke + * @author Mark Paluch */ +@SuppressWarnings("NullAway") public class MongoRepositoryFactoryBean, S, ID extends Serializable> extends RepositoryFactoryBeanSupport { private @Nullable MongoOperations operations; + private MongoRepositoryFragmentsContributor repositoryFragmentsContributor = MongoRepositoryFragmentsContributor.DEFAULT; private boolean createIndexesForQueryMethods = false; private boolean mappingContextConfigured = false; @@ -56,6 +59,22 @@ public void setMongoOperations(MongoOperations operations) { this.operations = operations; } + @Override + public MongoRepositoryFragmentsContributor getRepositoryFragmentsContributor() { + return repositoryFragmentsContributor; + } + + /** + * Configures the {@link MongoRepositoryFragmentsContributor} to contribute built-in fragment functionality to the + * repository. + * + * @param repositoryFragmentsContributor must not be {@literal null}. + * @since 5.0 + */ + public void setRepositoryFragmentsContributor(MongoRepositoryFragmentsContributor repositoryFragmentsContributor) { + this.repositoryFragmentsContributor = repositoryFragmentsContributor; + } + /** * Configures whether to automatically create indexes for the properties referenced in a query method. * @@ -75,7 +94,8 @@ public void setMappingContext(MappingContext mappingContext) { @Override protected RepositoryFactorySupport createRepositoryFactory() { - RepositoryFactorySupport factory = getFactoryInstance(operations); + MongoRepositoryFactory factory = getFactoryInstance(operations); + factory.setFragmentsContributor(repositoryFragmentsContributor); if (createIndexesForQueryMethods) { factory.addQueryCreationListener( @@ -91,7 +111,7 @@ protected RepositoryFactorySupport createRepositoryFactory() { * @param operations * @return */ - protected RepositoryFactorySupport getFactoryInstance(MongoOperations operations) { + protected MongoRepositoryFactory getFactoryInstance(MongoOperations operations) { return new MongoRepositoryFactory(operations); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributor.java new file mode 100644 index 0000000000..6d4a409724 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributor.java @@ -0,0 +1,78 @@ +/* + * 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.mongodb.repository.support; + +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; +import org.springframework.util.Assert; + +/** + * MongoDB-specific {@link RepositoryFragmentsContributor} contributing fragments based on the repository. + *

+ * Implementations must define a no-args constructor. + * + * @author Mark Paluch + * @since 5.0 + * @see QuerydslMongoPredicateExecutor + */ +public interface MongoRepositoryFragmentsContributor extends RepositoryFragmentsContributor { + + MongoRepositoryFragmentsContributor DEFAULT = QuerydslContributor.INSTANCE; + + /** + * Returns a composed {@code MongoRepositoryFragmentsContributor} that first applies this contributor to its inputs, + * and then applies the {@code after} contributor concatenating effectively both results. If evaluation of either + * contributors throws an exception, it is relayed to the caller of the composed contributor. + * + * @param after the contributor to apply after this contributor is applied. + * @return a composed contributor that first applies this contributor and then applies the {@code after} contributor. + */ + default MongoRepositoryFragmentsContributor andThen(MongoRepositoryFragmentsContributor after) { + + Assert.notNull(after, "MongoRepositoryFragmentsContributor must not be null"); + + return new MongoRepositoryFragmentsContributor() { + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + MongoEntityInformation entityInformation, MongoOperations operations) { + return MongoRepositoryFragmentsContributor.this.contribute(metadata, entityInformation, operations) + .append(after.contribute(metadata, entityInformation, operations)); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + return MongoRepositoryFragmentsContributor.this.describe(metadata).append(after.describe(metadata)); + } + }; + } + + /** + * Creates {@link RepositoryComposition.RepositoryFragments} based on {@link RepositoryMetadata} to add + * MongoDB-specific extensions. + * + * @param metadata repository metadata. + * @param entityInformation must not be {@literal null}. + * @param operations must not be {@literal null}. + * @return {@link RepositoryComposition.RepositoryFragments} to be added to the repository. + */ + RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + MongoEntityInformation entityInformation, MongoOperations operations); + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslContributor.java new file mode 100644 index 0000000000..e8460f3697 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslContributor.java @@ -0,0 +1,70 @@ +/* + * 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.mongodb.repository.support; + +import static org.springframework.data.querydsl.QuerydslUtils.*; + +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; + +/** + * MongoDB-specific {@link RepositoryFragmentsContributor} contributing Querydsl fragments if a repository implements + * {@link QuerydslPredicateExecutor}. + * + * @author Mark Paluch + * @since 5.0 + * @see QuerydslMongoPredicateExecutor + */ +enum QuerydslContributor implements MongoRepositoryFragmentsContributor { + + INSTANCE; + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + MongoEntityInformation entityInformation, MongoOperations operations) { + + if (isQuerydslRepository(metadata)) { + + QuerydslMongoPredicateExecutor executor = new QuerydslMongoPredicateExecutor<>(entityInformation, operations); + + return RepositoryComposition.RepositoryFragments + .of(RepositoryFragment.implemented(QuerydslPredicateExecutor.class, executor)); + } + + return RepositoryComposition.RepositoryFragments.empty(); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + + if (isQuerydslRepository(metadata)) { + return RepositoryComposition.RepositoryFragments + .of(RepositoryFragment.structural(QuerydslPredicateExecutor.class, QuerydslMongoPredicateExecutor.class)); + } + + return RepositoryComposition.RepositoryFragments.empty(); + } + + private static boolean isQuerydslRepository(RepositoryMetadata metadata) { + return QUERY_DSL_PRESENT && QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java index ec845510ce..833ce69458 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java @@ -23,6 +23,7 @@ import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -245,12 +246,12 @@ protected FluentQuerydsl create(Predicate predicate, Sort sort, int limit } @Override - public T oneValue() { + public @Nullable T oneValue() { return createQuery().fetchOne(); } @Override - public T firstValue() { + public @Nullable T firstValue() { return createQuery().fetchFirst(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java index 3edfcdd2db..ce9820a4d9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java @@ -15,13 +15,11 @@ */ package org.springframework.data.mongodb.repository.support; -import static org.springframework.data.querydsl.QuerydslUtils.*; - -import java.io.Serializable; import java.lang.reflect.Method; import java.util.Optional; -import org.springframework.beans.BeansException; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanFactory; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.ReactiveMongoOperations; @@ -33,21 +31,18 @@ import org.springframework.data.mongodb.repository.query.ReactivePartTreeMongoQuery; import org.springframework.data.mongodb.repository.query.ReactiveStringBasedAggregation; import org.springframework.data.mongodb.repository.query.ReactiveStringBasedMongoQuery; +import org.springframework.data.mongodb.repository.query.ReactiveVectorSearchAggregation; import org.springframework.data.projection.ProjectionFactory; -import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport; import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; -import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -63,6 +58,7 @@ public class ReactiveMongoRepositoryFactory extends ReactiveRepositoryFactorySup private final CrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor(); private final ReactiveMongoOperations operations; private final MappingContext, MongoPersistentProperty> mappingContext; + private ReactiveMongoRepositoryFragmentsContributor fragmentsContributor = ReactiveMongoRepositoryFragmentsContributor.DEFAULT; @Nullable private QueryMethodValueEvaluationContextAccessor accessor; /** @@ -77,19 +73,30 @@ public ReactiveMongoRepositoryFactory(ReactiveMongoOperations mongoOperations) { this.operations = mongoOperations; this.mappingContext = mongoOperations.getConverter().getMappingContext(); - setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT); addRepositoryProxyPostProcessor(crudMethodMetadataPostProcessor); } + /** + * Configures the {@link ReactiveMongoRepositoryFragmentsContributor} to be used. Defaults to + * {@link ReactiveMongoRepositoryFragmentsContributor#DEFAULT}. + * + * @param fragmentsContributor + * @since 5.0 + */ + public void setFragmentsContributor(ReactiveMongoRepositoryFragmentsContributor fragmentsContributor) { + this.fragmentsContributor = fragmentsContributor; + } + @Override - public void setBeanClassLoader(ClassLoader classLoader) { + public void setBeanClassLoader(@Nullable ClassLoader classLoader) { super.setBeanClassLoader(classLoader); crudMethodMetadataPostProcessor.setBeanClassLoader(classLoader); } @Override - protected ProjectionFactory getProjectionFactory(ClassLoader classLoader, BeanFactory beanFactory) { + @SuppressWarnings("NullAway") + protected ProjectionFactory getProjectionFactory(@Nullable ClassLoader classLoader, @Nullable BeanFactory beanFactory) { return this.operations.getConverter().getProjectionFactory(); } @@ -98,30 +105,26 @@ protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { return SimpleReactiveMongoRepository.class; } + /** + * Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add Mongo-specific extensions. + * Typically, adds a {@link ReactiveQuerydslContributor} if the repository interface uses Querydsl. + *

+ * Built-in fragment contribution can be customized by configuring + * {@link ReactiveMongoRepositoryFragmentsContributor}. + * + * @param metadata repository metadata. + * @return {@link RepositoryFragments} to be added to the repository. + */ @Override protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { - - RepositoryFragments fragments = RepositoryFragments.empty(); - - boolean isQueryDslRepository = QUERY_DSL_PRESENT - && ReactiveQuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); - - if (isQueryDslRepository) { - - MongoEntityInformation entityInformation = getEntityInformation(metadata.getDomainType(), - metadata); - - fragments = fragments.append(RepositoryFragment - .implemented(instantiateClass(ReactiveQuerydslMongoPredicateExecutor.class, entityInformation, operations))); - } - - return fragments; + return fragmentsContributor.contribute(metadata, getEntityInformation(metadata), + operations); } @Override protected Object getTargetRepository(RepositoryInformation information) { - MongoEntityInformation entityInformation = getEntityInformation(information.getDomainType(), + MongoEntityInformation entityInformation = getEntityInformation( information); Object targetRepository = getTargetRepositoryViaReflection(information, entityInformation, operations); @@ -132,24 +135,25 @@ protected Object getTargetRepository(RepositoryInformation information) { return targetRepository; } - @Override protected Optional getQueryLookupStrategy(Key key, + @Override + @SuppressWarnings("NullAway") + protected Optional getQueryLookupStrategy(Key key, ValueExpressionDelegate valueExpressionDelegate) { return Optional.of(new MongoQueryLookupStrategy(operations, mappingContext, valueExpressionDelegate)); } + @Deprecated @Override public MongoEntityInformation getEntityInformation(Class domainClass) { - return getEntityInformation(domainClass, null); + MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainClass); + return MongoEntityInformationSupport.entityInformationFor(entity, null); } - @SuppressWarnings("unchecked") - private MongoEntityInformation getEntityInformation(Class domainClass, - @Nullable RepositoryMetadata metadata) { - - MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainClass); + @Override + public MongoEntityInformation getEntityInformation(RepositoryMetadata metadata) { - return new MappingMongoEntityInformation<>((MongoPersistentEntity) entity, - metadata != null ? (Class) metadata.getIdType() : null); + MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(metadata.getDomainType()); + return MongoEntityInformationSupport.entityInformationFor(entity, metadata.getIdType()); } /** @@ -159,8 +163,8 @@ private MongoEntityInformation getEntityInformation(Class doma * @author Christoph Strobl */ private record MongoQueryLookupStrategy(ReactiveMongoOperations operations, - MappingContext, MongoPersistentProperty> mappingContext, - ValueExpressionDelegate delegate) implements QueryLookupStrategy { + MappingContext, MongoPersistentProperty> mappingContext, + ValueExpressionDelegate delegate) implements QueryLookupStrategy { @Override public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, @@ -174,6 +178,8 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, if (namedQueries.hasQuery(namedQueryName)) { String namedQuery = namedQueries.getQuery(namedQueryName); return new ReactiveStringBasedMongoQuery(namedQuery, queryMethod, operations, delegate); + } else if (queryMethod.hasAnnotatedVectorSearch()) { + return new ReactiveVectorSearchAggregation(queryMethod, operations, delegate); } else if (queryMethod.hasAnnotatedAggregation()) { return new ReactiveStringBasedAggregation(queryMethod, operations, delegate); } else if (queryMethod.hasAnnotatedQuery()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java index 4f9c0d945c..40de5213aa 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java @@ -16,18 +16,14 @@ package org.springframework.data.mongodb.repository.support; import java.io.Serializable; -import java.util.Optional; -import org.springframework.beans.factory.ListableBeanFactory; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.ReactiveMongoOperations; import org.springframework.data.mongodb.core.index.IndexOperationsAdapter; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.core.support.RepositoryFactorySupport; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -44,6 +40,7 @@ public class ReactiveMongoRepositoryFactoryBean, S, extends RepositoryFactoryBeanSupport { private @Nullable ReactiveMongoOperations operations; + private ReactiveMongoRepositoryFragmentsContributor repositoryFragmentsContributor = ReactiveMongoRepositoryFragmentsContributor.DEFAULT; private boolean createIndexesForQueryMethods = false; private boolean mappingContextConfigured = false; @@ -65,6 +62,23 @@ public void setReactiveMongoOperations(@Nullable ReactiveMongoOperations operati this.operations = operations; } + @Override + public ReactiveMongoRepositoryFragmentsContributor getRepositoryFragmentsContributor() { + return repositoryFragmentsContributor; + } + + /** + * Configures the {@link MongoRepositoryFragmentsContributor} to contribute built-in fragment functionality to the + * repository. + * + * @param repositoryFragmentsContributor must not be {@literal null}. + * @since 5.0 + */ + public void setRepositoryFragmentsContributor( + ReactiveMongoRepositoryFragmentsContributor repositoryFragmentsContributor) { + this.repositoryFragmentsContributor = repositoryFragmentsContributor; + } + /** * Configures whether to automatically create indexes for the properties referenced in a query method. * @@ -82,9 +96,11 @@ public void setMappingContext(MappingContext mappingContext) { } @Override + @SuppressWarnings("NullAway") protected RepositoryFactorySupport createRepositoryFactory() { - RepositoryFactorySupport factory = getFactoryInstance(operations); + ReactiveMongoRepositoryFactory factory = getFactoryInstance(operations); + factory.setFragmentsContributor(repositoryFragmentsContributor); if (createIndexesForQueryMethods) { factory.addQueryCreationListener(new IndexEnsuringQueryCreationListener( @@ -94,19 +110,13 @@ protected RepositoryFactorySupport createRepositoryFactory() { return factory; } - @Override - protected Optional createDefaultQueryMethodEvaluationContextProvider( - ListableBeanFactory beanFactory) { - return Optional.of(new ReactiveExtensionAwareQueryMethodEvaluationContextProvider(beanFactory)); - } - /** * Creates and initializes a {@link RepositoryFactorySupport} instance. * * @param operations * @return */ - protected RepositoryFactorySupport getFactoryInstance(ReactiveMongoOperations operations) { + protected ReactiveMongoRepositoryFactory getFactoryInstance(ReactiveMongoOperations operations) { return new ReactiveMongoRepositoryFactory(operations); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributor.java new file mode 100644 index 0000000000..fdf3c3649e --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributor.java @@ -0,0 +1,78 @@ +/* + * 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.mongodb.repository.support; + +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; +import org.springframework.util.Assert; + +/** + * Reactive MongoDB-specific {@link RepositoryFragmentsContributor} contributing fragments based on the repository. + *

+ * Implementations must define a no-args constructor. + * + * @author Mark Paluch + * @since 5.0 + * @see ReactiveQuerydslMongoPredicateExecutor + */ +public interface ReactiveMongoRepositoryFragmentsContributor extends RepositoryFragmentsContributor { + + ReactiveMongoRepositoryFragmentsContributor DEFAULT = ReactiveQuerydslContributor.INSTANCE; + + /** + * Returns a composed {@code ReactiveMongoRepositoryFragmentsContributor} that first applies this contributor to its + * inputs, and then applies the {@code after} contributor concatenating effectively both results. If evaluation of + * either contributors throws an exception, it is relayed to the caller of the composed contributor. + * + * @param after the contributor to apply after this contributor is applied. + * @return a composed contributor that first applies this contributor and then applies the {@code after} contributor. + */ + default ReactiveMongoRepositoryFragmentsContributor andThen(ReactiveMongoRepositoryFragmentsContributor after) { + + Assert.notNull(after, "ReactiveMongoRepositoryFragmentsContributor must not be null"); + + return new ReactiveMongoRepositoryFragmentsContributor() { + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + MongoEntityInformation entityInformation, ReactiveMongoOperations operations) { + return ReactiveMongoRepositoryFragmentsContributor.this.contribute(metadata, entityInformation, operations) + .append(after.contribute(metadata, entityInformation, operations)); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + return ReactiveMongoRepositoryFragmentsContributor.this.describe(metadata).append(after.describe(metadata)); + } + }; + } + + /** + * Creates {@link RepositoryComposition.RepositoryFragments} based on {@link RepositoryMetadata} to add + * MongoDB-specific extensions. + * + * @param metadata repository metadata. + * @param entityInformation must not be {@literal null}. + * @param operations must not be {@literal null}. + * @return {@link RepositoryComposition.RepositoryFragments} to be added to the repository. + */ + RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + MongoEntityInformation entityInformation, ReactiveMongoOperations operations); + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslContributor.java new file mode 100644 index 0000000000..2cea75cb44 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslContributor.java @@ -0,0 +1,73 @@ +/* + * 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.mongodb.repository.support; + +import static org.springframework.data.querydsl.QuerydslUtils.*; + +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; + +/** + * Reactive MongoDB-specific {@link RepositoryFragmentsContributor} contributing Querydsl fragments if a repository + * implements {@link QuerydslPredicateExecutor}. + * + * @author Mark Paluch + * @since 5.0 + * @see ReactiveQuerydslMongoPredicateExecutor + */ +enum ReactiveQuerydslContributor implements ReactiveMongoRepositoryFragmentsContributor { + + INSTANCE; + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + MongoEntityInformation entityInformation, ReactiveMongoOperations operations) { + + if (isQuerydslRepository(metadata)) { + + ReactiveQuerydslPredicateExecutor executor = new ReactiveQuerydslMongoPredicateExecutor<>(entityInformation, + operations); + + return RepositoryComposition.RepositoryFragments + .of(RepositoryFragment.implemented(ReactiveQuerydslPredicateExecutor.class, executor)); + } + + return RepositoryComposition.RepositoryFragments.empty(); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + + if (isQuerydslRepository(metadata)) { + return RepositoryComposition.RepositoryFragments.of(RepositoryFragment + .structural(ReactiveQuerydslPredicateExecutor.class, ReactiveQuerydslMongoPredicateExecutor.class)); + } + + return RepositoryComposition.RepositoryFragments.empty(); + } + + private static boolean isQuerydslRepository(RepositoryMetadata metadata) { + return QUERY_DSL_PRESENT + && ReactiveQuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java index cf5191fd42..a86ada0aad 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java @@ -25,6 +25,7 @@ import java.util.function.Consumer; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.ScrollPosition; @@ -36,7 +37,6 @@ import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; @@ -304,7 +304,7 @@ static class NoMatchException extends RuntimeException { @Override public synchronized Throwable fillInStackTrace() { - return null; + return this; } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java index 2f4c30ee7a..7e6e2fc82e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java @@ -27,6 +27,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.domain.Example; import org.springframework.data.domain.Page; @@ -47,7 +48,6 @@ import org.springframework.data.support.PageableExecutionUtils; import org.springframework.data.util.StreamUtils; import org.springframework.data.util.Streamable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.mongodb.ReadPreference; @@ -427,12 +427,12 @@ protected FluentQueryByExample create(Example predicate, Sort sort, } @Override - public T oneValue() { + public @Nullable T oneValue() { return createQuery().oneValue(); } @Override - public T firstValue() { + public @Nullable T firstValue() { return createQuery().firstValue(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java index 1c1df2c9a1..7e4a3aa665 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java @@ -30,6 +30,7 @@ import java.util.function.Function; import java.util.function.UnaryOperator; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.dao.IncorrectResultSizeDataAccessException; @@ -49,7 +50,6 @@ import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; import org.springframework.data.repository.query.FluentQuery; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.mongodb.ReadPreference; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java index 0ef6c38744..24a9342ca1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java @@ -22,7 +22,7 @@ import java.util.stream.Stream; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; @@ -36,7 +36,6 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.repository.util.SliceUtils; import org.springframework.data.support.PageableExecutionUtils; -import org.springframework.lang.Nullable; import com.mysema.commons.lang.CloseableIterator; import com.mysema.commons.lang.EmptyCloseableIterator; @@ -47,6 +46,7 @@ import com.querydsl.core.types.Expression; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Predicate; +import org.springframework.lang.Contract; /** * Spring Data specific simple {@link com.querydsl.core.Fetchable} {@link com.querydsl.core.SimpleQuery Query} @@ -200,7 +200,7 @@ public Slice fetchSlice(Pageable pageable) { } @Override - public T fetchFirst() { + public @Nullable T fetchFirst() { try { return find.matching(createQuery()).firstValue(); } catch (RuntimeException e) { @@ -209,7 +209,7 @@ public T fetchFirst() { } @Override - public T fetchOne() { + public @Nullable T fetchOne() { try { return find.matching(createQuery()).oneValue(); } catch (RuntimeException e) { @@ -279,7 +279,8 @@ protected List getIds(Class targetType, Predicate condition) { return mongoOperations.findDistinct(query, FieldName.ID.name(), targetType, Object.class); } - private static T handleException(RuntimeException e, T defaultValue) { + @Contract("_, !null -> !null") + private static @Nullable T handleException(RuntimeException e, @Nullable T defaultValue) { if (e.getClass().getName().endsWith("$NoResults")) { return defaultValue; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuerySupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuerySupport.java index a64f666f3f..64ea5f2384 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuerySupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuerySupport.java @@ -27,6 +27,8 @@ import com.querydsl.core.types.OrderSpecifier; import com.querydsl.mongodb.document.AbstractMongodbQuery; import com.querydsl.mongodb.document.MongodbDocumentSerializer; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; /** * Support query type to augment Spring Data-specific {@link #toString} representations and diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java index d9a550a0f7..756d04d0c2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java @@ -20,6 +20,8 @@ import java.util.regex.Pattern; import org.bson.Document; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.convert.QueryMapper; @@ -27,7 +29,6 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -48,6 +49,7 @@ * @author Christoph Strobl * @author Mark Paluch */ +@NullUnmarked class SpringDataMongodbSerializer extends MongodbDocumentSerializer { private static final String ID_KEY = FieldName.ID.name(); @@ -146,8 +148,7 @@ protected boolean isId(Path arg) { } @Override - @Nullable - protected Object convert(@Nullable Path path, @Nullable Constant constant) { + protected @Nullable Object convert(@Nullable Path path, @Nullable Constant constant) { if (constant == null) { return null; @@ -191,8 +192,7 @@ protected Object convert(@Nullable Path path, @Nullable Constant constant) return asReference(constant.getConstant(), path); } - @Nullable - private MongoPersistentProperty getPropertyFor(Path path) { + private @Nullable MongoPersistentProperty getPropertyFor(Path path) { Path parent = path.getMetadata().getParent(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/package-info.java index 1d0b8beeba..42cd5a0b18 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/package-info.java @@ -1,6 +1,6 @@ /** * Support infrastructure for query derivation of MongoDB specific repositories. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.repository.support; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java index cbbd4a37a9..dc51da84ed 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java @@ -29,22 +29,52 @@ import java.util.function.Function; import java.util.stream.StreamSupport; -import org.bson.*; +import org.bson.AbstractBsonWriter; +import org.bson.BSONObject; +import org.bson.BsonArray; +import org.bson.BsonBinary; +import org.bson.BsonBinarySubType; +import org.bson.BsonBoolean; +import org.bson.BsonContextType; +import org.bson.BsonDateTime; +import org.bson.BsonDbPointer; +import org.bson.BsonDecimal128; +import org.bson.BsonDouble; +import org.bson.BsonInt32; +import org.bson.BsonInt64; +import org.bson.BsonJavaScript; +import org.bson.BsonNull; +import org.bson.BsonObjectId; +import org.bson.BsonReader; +import org.bson.BsonRegularExpression; +import org.bson.BsonString; +import org.bson.BsonSymbol; +import org.bson.BsonTimestamp; +import org.bson.BsonUndefined; +import org.bson.BsonValue; +import org.bson.BsonWriter; +import org.bson.BsonWriterSettings; +import org.bson.Document; import org.bson.codecs.Codec; +import org.bson.codecs.DecoderContext; import org.bson.codecs.DocumentCodec; import org.bson.codecs.EncoderContext; import org.bson.codecs.configuration.CodecConfigurationException; +import org.bson.codecs.configuration.CodecRegistries; import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; import org.bson.json.JsonParseException; import org.bson.types.Binary; import org.bson.types.Decimal128; import org.bson.types.ObjectId; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; import org.springframework.data.mongodb.CodecRegistryProvider; import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.mapping.FieldName.Type; -import org.springframework.lang.Nullable; +import org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -72,9 +102,12 @@ public class BsonUtils { */ public static final Document EMPTY_DOCUMENT = new EmptyDocument(); + private static final CodecRegistry JSON_CODEC_REGISTRY = CodecRegistries.fromRegistries( + MongoClientSettings.getDefaultCodecRegistry(), CodecRegistries.fromCodecs(new PlaceholderCodec())); + @SuppressWarnings("unchecked") - @Nullable - public static T get(Bson bson, String key) { + @Contract("null, _ -> null") + public static @Nullable T get(@Nullable Bson bson, String key) { return (T) asMap(bson).get(key); } @@ -85,7 +118,7 @@ public static T get(Bson bson, String key) { * @param bson * @return */ - public static Map asMap(Bson bson) { + public static Map asMap(@Nullable Bson bson) { return asMap(bson, MongoClientSettings.getDefaultCodecRegistry()); } @@ -126,7 +159,7 @@ public static Map asMap(@Nullable Bson bson, CodecRegistry codec * @return * @since 3.2.5 */ - public static Document asDocument(Bson bson) { + public static Document asDocument(@Nullable Bson bson) { return asDocument(bson, MongoClientSettings.getDefaultCodecRegistry()); } @@ -140,7 +173,7 @@ public static Document asDocument(Bson bson) { * @return never {@literal null}. * @since 4.0 */ - public static Document asDocument(Bson bson, CodecRegistry codecRegistry) { + public static Document asDocument(@Nullable Bson bson, CodecRegistry codecRegistry) { Map map = asMap(bson, codecRegistry); @@ -304,7 +337,7 @@ public static Object toJavaType(BsonValue value) { case BINARY -> { BsonBinary binary = value.asBinary(); - if(binary.getType() != BsonBinarySubType.VECTOR.getValue()) { + if (binary.getType() != BsonBinarySubType.VECTOR.getValue()) { yield binary.getData(); } yield value.asBinary().asVector(); @@ -326,14 +359,14 @@ public static Object toJavaType(BsonValue value) { * @throws IllegalArgumentException if {@literal source} does not correspond to a {@link BsonValue} type. * @since 3.0 */ - public static BsonValue simpleToBsonValue(Object source) { + public static BsonValue simpleToBsonValue(@Nullable Object source) { return simpleToBsonValue(source, MongoClientSettings.getDefaultCodecRegistry()); } /** * Convert a given simple value (eg. {@link String}, {@link Long}) to its corresponding {@link BsonValue}. * - * @param source must not be {@literal null}. + * @param source can be {@literal null}. * @param codecRegistry The {@link CodecRegistry} used as a fallback to convert types using native {@link Codec}. Must * not be {@literal null}. * @return the corresponding {@link BsonValue} representation. @@ -341,7 +374,12 @@ public static BsonValue simpleToBsonValue(Object source) { * @since 4.2 */ @SuppressWarnings("unchecked") - public static BsonValue simpleToBsonValue(Object source, CodecRegistry codecRegistry) { + @Contract("null, _ -> !null") + public static BsonValue simpleToBsonValue(@Nullable Object source, CodecRegistry codecRegistry) { + + if(source == null) { + return BsonNull.VALUE; + } if (source instanceof BsonValue bsonValue) { return bsonValue; @@ -398,7 +436,9 @@ public static BsonValue simpleToBsonValue(Object source, CodecRegistry codecRegi BsonCapturingWriter writer = new BsonCapturingWriter(value.getClass()); codec.encode(writer, value, ObjectUtils.isArray(value) || value instanceof Collection ? EncoderContext.builder().build() : null); - return writer.getCapturedValue(); + Object captured = writer.getCapturedValue(); + return captured instanceof BsonValue bv ? bv : BsonNull.VALUE; + } catch (CodecConfigurationException e) { throw new IllegalArgumentException( String.format("Unable to convert %s to BsonValue.", source != null ? source.getClass().getName() : "null")); @@ -450,8 +490,7 @@ public static Document toDocumentOrElse(String source, Function false") public static boolean isJsonDocument(@Nullable String value) { if (!StringUtils.hasText(value)) { @@ -488,6 +528,7 @@ public static boolean isJsonDocument(@Nullable String value) { * @return {@literal true} if the given value looks like a json array. * @since 3.0 */ + @Contract("null -> false") public static boolean isJsonArray(@Nullable String value) { return StringUtils.hasText(value) && (value.startsWith("[") && value.endsWith("]")); } @@ -525,8 +566,7 @@ public static Document parse(String json, @Nullable CodecRegistryProvider codecR * @return can be {@literal null}. * @since 3.0.8 */ - @Nullable - public static Object resolveValue(Bson bson, String key) { + public static @Nullable Object resolveValue(Bson bson, String key) { return resolveValue(asMap(bson), key); } @@ -541,7 +581,7 @@ public static Object resolveValue(Bson bson, String key) { * @return can be {@literal null}. * @since 4.2 */ - public static Object resolveValue(Bson bson, FieldName fieldName) { + public static @Nullable Object resolveValue(Bson bson, FieldName fieldName) { return resolveValue(asMap(bson), fieldName); } @@ -556,8 +596,7 @@ public static Object resolveValue(Bson bson, FieldName fieldName) { * @return can be {@literal null}. * @since 4.2 */ - @Nullable - public static Object resolveValue(Map source, FieldName fieldName) { + public static @Nullable Object resolveValue(Map source, FieldName fieldName) { if (fieldName.isKey()) { return source.get(fieldName.name()); @@ -590,8 +629,7 @@ public static Object resolveValue(Map source, FieldName fieldNam * @return can be {@literal null}. * @since 4.1 */ - @Nullable - public static Object resolveValue(Map source, String key) { + public static @Nullable Object resolveValue(Map source, String key) { if (source.containsKey(key)) { return source.get(key); @@ -643,9 +681,9 @@ public static boolean hasValue(Bson bson, String key) { * @param source can be {@literal null}. * @return can be {@literal null}. */ - @Nullable @SuppressWarnings("unchecked") - private static Map getAsMap(Object source) { + @Contract("null -> null") + private static @Nullable Map getAsMap(@Nullable Object source) { if (source instanceof Document document) { return document; @@ -745,8 +783,43 @@ public static Document mapEntries(Document source, Function JSON_CODEC_REGISTRY.get(Document.class).encode(new SpringJsonWriter(sink), document, + EncoderContext.builder().build()); + } + + /** + * Interface to pipe json rendering to a given sink. + * + * @since 5.0 + */ + public interface JsonWriter { + + /** + * Write the json output to the given sink. + * + * @param sink the output target + */ + void to(StringBuffer sink); + + default String toJsonString() { + + StringBuffer buffer = new StringBuffer(); + to(buffer); + return buffer.toString(); + } + } + + @Contract("null -> null") + private static @Nullable String toJson(@Nullable Object value) { if (value == null) { return null; @@ -957,4 +1030,34 @@ public void flush() { values.clear(); } } + + /** + * Internal {@link Codec} implementation to write + * {@link org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder placeholders}. + * + * @since 5.0 + * @author Christoph Strobl + */ + @NullUnmarked + static class PlaceholderCodec implements Codec { + + @Override + public Placeholder decode(BsonReader reader, DecoderContext decoderContext) { + return null; + } + + @Override + public void encode(BsonWriter writer, Placeholder value, EncoderContext encoderContext) { + if (writer instanceof SpringJsonWriter sjw) { + sjw.writePlaceholder(value.toString()); + } else { + writer.writeString(value.toString()); + } + } + + @Override + public Class getEncoderClass() { + return Placeholder.class; + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DotPath.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DotPath.java index 191c7d24d3..549c7ff720 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DotPath.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DotPath.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.util; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.StringUtils; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DurationUtil.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DurationUtil.java index 67255b878a..78eb59a461 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DurationUtil.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DurationUtil.java @@ -18,6 +18,7 @@ import java.time.Duration; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; import org.springframework.core.env.Environment; import org.springframework.data.expression.ValueEvaluationContext; import org.springframework.data.expression.ValueExpression; @@ -25,7 +26,6 @@ import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.format.datetime.standard.DurationFormatterUtils; -import org.springframework.lang.Nullable; /** * Helper to evaluate Duration from expressions. @@ -70,13 +70,11 @@ public static Duration evaluate(String value, ValueEvaluationContext evaluationC public static Duration evaluate(String value, Supplier evaluationContext) { return evaluate(value, new ValueEvaluationContext() { - @Nullable @Override public Environment getEnvironment() { - return null; + throw new IllegalStateException(); } - @Nullable @Override public EvaluationContext getEvaluationContext() { return evaluationContext.get(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/EmptyDocument.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/EmptyDocument.java index ffc97402fe..23ea9409cc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/EmptyDocument.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/EmptyDocument.java @@ -66,9 +66,8 @@ public boolean replace(String key, Object oldValue, Object newValue) { throw new UnsupportedOperationException(); } - @Nullable @Override - public Object replace(String key, Object value) { + public @Nullable Object replace(String key, Object value) { throw new UnsupportedOperationException(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java index 8fc4b108ff..fbbba59e8f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java @@ -15,12 +15,9 @@ */ package org.springframework.data.mongodb.util; -import java.lang.reflect.Field; - +import org.jspecify.annotations.Nullable; import org.springframework.data.util.Version; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; -import org.springframework.util.ReflectionUtils; import com.mongodb.internal.build.MongoDriverVersion; @@ -94,14 +91,12 @@ private static Version getMongoDbDriverVersion(ClassLoader classLoader) { return version == null ? guessDriverVersionFromClassPath(classLoader) : version; } - @Nullable - private static Version getVersionFromPackage(ClassLoader classLoader) { + private static @Nullable Version getVersionFromPackage(ClassLoader classLoader) { if (ClassUtils.isPresent("com.mongodb.internal.build.MongoDriverVersion", classLoader)) { try { - Field field = ReflectionUtils.findField(MongoDriverVersion.class, "VERSION"); - return field != null ? Version.parse("" + field.get(null)) : null; - } catch (ReflectiveOperationException | IllegalArgumentException exception) { + return Version.parse(MongoDriverVersion.VERSION); + } catch (IllegalArgumentException exception) { // well not much we can do, right? } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java deleted file mode 100644 index f85be98c1f..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java +++ /dev/null @@ -1,380 +0,0 @@ -/* - * 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.mongodb.util; - -import java.lang.reflect.Method; -import java.net.InetSocketAddress; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Publisher; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.ReflectionUtils; - -import com.mongodb.MongoClientSettings; -import com.mongodb.MongoClientSettings.Builder; -import com.mongodb.ServerAddress; -import com.mongodb.client.ClientSession; -import com.mongodb.client.MapReduceIterable; -import com.mongodb.client.MongoDatabase; -import com.mongodb.client.MongoIterable; -import com.mongodb.client.model.IndexOptions; -import com.mongodb.reactivestreams.client.MapReducePublisher; - -/** - * Compatibility adapter to bridge functionality across different MongoDB driver versions. - *

- * This class is for internal use within the framework and should not be used by applications. - * - * @author Christoph Strobl - * @since 4.3 - */ -public class MongoCompatibilityAdapter { - - private static final String NO_LONGER_SUPPORTED = "%s is no longer supported on Mongo Client 5 or newer"; - - private static final @Nullable Method getStreamFactoryFactory = ReflectionUtils.findMethod(MongoClientSettings.class, - "getStreamFactoryFactory"); - - private static final @Nullable Method setBucketSize = ReflectionUtils.findMethod(IndexOptions.class, "bucketSize", - Double.class); - - /** - * Return a compatibility adapter for {@link MongoClientSettings.Builder}. - * - * @param builder - * @return - */ - public static ClientSettingsBuilderAdapter clientSettingsBuilderAdapter(MongoClientSettings.Builder builder) { - return new MongoStreamFactoryFactorySettingsConfigurer(builder)::setStreamFactory; - } - - /** - * Return a compatibility adapter for {@link MongoClientSettings}. - * - * @param clientSettings - * @return - */ - public static ClientSettingsAdapter clientSettingsAdapter(MongoClientSettings clientSettings) { - return new ClientSettingsAdapter() { - @Override - public T getStreamFactoryFactory() { - - if (MongoClientVersion.isVersion5orNewer() || getStreamFactoryFactory == null) { - return null; - } - - return (T) ReflectionUtils.invokeMethod(getStreamFactoryFactory, clientSettings); - } - }; - } - - /** - * Return a compatibility adapter for {@link IndexOptions}. - * - * @param options - * @return - */ - public static IndexOptionsAdapter indexOptionsAdapter(IndexOptions options) { - return bucketSize -> { - - if (MongoClientVersion.isVersion5orNewer() || setBucketSize == null) { - throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("IndexOptions.bucketSize")); - } - - ReflectionUtils.invokeMethod(setBucketSize, options, bucketSize); - }; - } - - /** - * Return a compatibility adapter for {@code MapReduceIterable}. - * - * @param iterable - * @return - */ - @SuppressWarnings("deprecation") - public static MapReduceIterableAdapter mapReduceIterableAdapter(Object iterable) { - return sharded -> { - - if (MongoClientVersion.isVersion5orNewer()) { - throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("sharded")); - } - - // Use MapReduceIterable to avoid package-protected access violations to - // com.mongodb.client.internal.MapReduceIterableImpl - Method shardedMethod = ReflectionUtils.findMethod(MapReduceIterable.class, "sharded", boolean.class); - ReflectionUtils.invokeMethod(shardedMethod, iterable, sharded); - }; - } - - /** - * Return a compatibility adapter for {@code MapReducePublisher}. - * - * @param publisher - * @return - */ - @SuppressWarnings("deprecation") - public static MapReducePublisherAdapter mapReducePublisherAdapter(Object publisher) { - return sharded -> { - - if (MongoClientVersion.isVersion5orNewer()) { - throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("sharded")); - } - - // Use MapReducePublisher to avoid package-protected access violations to MapReducePublisherImpl - Method shardedMethod = ReflectionUtils.findMethod(MapReducePublisher.class, "sharded", boolean.class); - ReflectionUtils.invokeMethod(shardedMethod, publisher, sharded); - }; - } - - /** - * Return a compatibility adapter for {@link ServerAddress}. - * - * @param serverAddress - * @return - */ - public static ServerAddressAdapter serverAddressAdapter(ServerAddress serverAddress) { - return () -> { - - if (MongoClientVersion.isVersion5orNewer()) { - return null; - } - - Method serverAddressMethod = ReflectionUtils.findMethod(ServerAddress.class, "getSocketAddress"); - Object value = ReflectionUtils.invokeMethod(serverAddressMethod, serverAddress); - return value != null ? InetSocketAddress.class.cast(value) : null; - }; - } - - public static MongoDatabaseAdapterBuilder mongoDatabaseAdapter() { - return MongoDatabaseAdapter::new; - } - - public static ReactiveMongoDatabaseAdapterBuilder reactiveMongoDatabaseAdapter() { - return ReactiveMongoDatabaseAdapter::new; - } - - public interface IndexOptionsAdapter { - void setBucketSize(double bucketSize); - } - - public interface ClientSettingsAdapter { - @Nullable - T getStreamFactoryFactory(); - } - - public interface ClientSettingsBuilderAdapter { - void setStreamFactoryFactory(T streamFactory); - } - - public interface MapReduceIterableAdapter { - void sharded(boolean sharded); - } - - public interface MapReducePublisherAdapter { - void sharded(boolean sharded); - } - - public interface ServerAddressAdapter { - @Nullable - InetSocketAddress getSocketAddress(); - } - - public interface MongoDatabaseAdapterBuilder { - MongoDatabaseAdapter forDb(com.mongodb.client.MongoDatabase db); - } - - @SuppressWarnings({ "unchecked", "DataFlowIssue" }) - public static class MongoDatabaseAdapter { - - @Nullable // - private static final Method LIST_COLLECTION_NAMES_METHOD; - - @Nullable // - private static final Method LIST_COLLECTION_NAMES_METHOD_SESSION; - - private static final Class collectionNamesReturnType; - - private final MongoDatabase db; - - static { - - if (MongoClientVersion.isSyncClientPresent()) { - - LIST_COLLECTION_NAMES_METHOD = ReflectionUtils.findMethod(MongoDatabase.class, "listCollectionNames"); - LIST_COLLECTION_NAMES_METHOD_SESSION = ReflectionUtils.findMethod(MongoDatabase.class, "listCollectionNames", - ClientSession.class); - - if (MongoClientVersion.isVersion5orNewer()) { - try { - collectionNamesReturnType = ClassUtils.forName("com.mongodb.client.ListCollectionNamesIterable", - MongoDatabaseAdapter.class.getClassLoader()); - } catch (ClassNotFoundException e) { - throw new IllegalStateException("Unable to load com.mongodb.client.ListCollectionNamesIterable", e); - } - } else { - try { - collectionNamesReturnType = ClassUtils.forName("com.mongodb.client.MongoIterable", - MongoDatabaseAdapter.class.getClassLoader()); - } catch (ClassNotFoundException e) { - throw new IllegalStateException("Unable to load com.mongodb.client.ListCollectionNamesIterable", e); - } - } - } else { - LIST_COLLECTION_NAMES_METHOD = null; - LIST_COLLECTION_NAMES_METHOD_SESSION = null; - collectionNamesReturnType = Object.class; - } - } - - public MongoDatabaseAdapter(MongoDatabase db) { - this.db = db; - } - - public Class> collectionNameIterableType() { - return (Class>) collectionNamesReturnType; - } - - public MongoIterable listCollectionNames() { - - Assert.state(LIST_COLLECTION_NAMES_METHOD != null, "No method listCollectionNames present for %s".formatted(db)); - return (MongoIterable) ReflectionUtils.invokeMethod(LIST_COLLECTION_NAMES_METHOD, db); - } - - public MongoIterable listCollectionNames(ClientSession clientSession) { - Assert.state(LIST_COLLECTION_NAMES_METHOD != null, - "No method listCollectionNames(ClientSession) present for %s".formatted(db)); - return (MongoIterable) ReflectionUtils.invokeMethod(LIST_COLLECTION_NAMES_METHOD_SESSION, db, - clientSession); - } - } - - public interface ReactiveMongoDatabaseAdapterBuilder { - ReactiveMongoDatabaseAdapter forDb(com.mongodb.reactivestreams.client.MongoDatabase db); - } - - @SuppressWarnings({ "unchecked", "DataFlowIssue" }) - public static class ReactiveMongoDatabaseAdapter { - - @Nullable // - private static final Method LIST_COLLECTION_NAMES_METHOD; - - @Nullable // - private static final Method LIST_COLLECTION_NAMES_METHOD_SESSION; - - private static final Class collectionNamesReturnType; - - private final com.mongodb.reactivestreams.client.MongoDatabase db; - - static { - - if (MongoClientVersion.isReactiveClientPresent()) { - - LIST_COLLECTION_NAMES_METHOD = ReflectionUtils - .findMethod(com.mongodb.reactivestreams.client.MongoDatabase.class, "listCollectionNames"); - LIST_COLLECTION_NAMES_METHOD_SESSION = ReflectionUtils.findMethod( - com.mongodb.reactivestreams.client.MongoDatabase.class, "listCollectionNames", - com.mongodb.reactivestreams.client.ClientSession.class); - - if (MongoClientVersion.isVersion5orNewer()) { - try { - collectionNamesReturnType = ClassUtils.forName( - "com.mongodb.reactivestreams.client.ListCollectionNamesPublisher", - ReactiveMongoDatabaseAdapter.class.getClassLoader()); - } catch (ClassNotFoundException e) { - throw new IllegalStateException("com.mongodb.reactivestreams.client.ListCollectionNamesPublisher", e); - } - } else { - try { - collectionNamesReturnType = ClassUtils.forName("org.reactivestreams.Publisher", - ReactiveMongoDatabaseAdapter.class.getClassLoader()); - } catch (ClassNotFoundException e) { - throw new IllegalStateException("org.reactivestreams.Publisher", e); - } - } - } else { - LIST_COLLECTION_NAMES_METHOD = null; - LIST_COLLECTION_NAMES_METHOD_SESSION = null; - collectionNamesReturnType = Object.class; - } - } - - ReactiveMongoDatabaseAdapter(com.mongodb.reactivestreams.client.MongoDatabase db) { - this.db = db; - } - - public Class> collectionNamePublisherType() { - return (Class>) collectionNamesReturnType; - - } - - public Publisher listCollectionNames() { - Assert.state(LIST_COLLECTION_NAMES_METHOD != null, "No method listCollectionNames present for %s".formatted(db)); - return (Publisher) ReflectionUtils.invokeMethod(LIST_COLLECTION_NAMES_METHOD, db); - } - - public Publisher listCollectionNames(com.mongodb.reactivestreams.client.ClientSession clientSession) { - Assert.state(LIST_COLLECTION_NAMES_METHOD != null, - "No method listCollectionNames(ClientSession) present for %s".formatted(db)); - return (Publisher) ReflectionUtils.invokeMethod(LIST_COLLECTION_NAMES_METHOD_SESSION, db, clientSession); - } - } - - static class MongoStreamFactoryFactorySettingsConfigurer { - - private static final Log logger = LogFactory.getLog(MongoStreamFactoryFactorySettingsConfigurer.class); - - private static final String STREAM_FACTORY_NAME = "com.mongodb.connection.StreamFactoryFactory"; - private static final boolean STREAM_FACTORY_PRESENT = ClassUtils.isPresent(STREAM_FACTORY_NAME, - MongoCompatibilityAdapter.class.getClassLoader()); - private final MongoClientSettings.Builder settingsBuilder; - - static boolean isStreamFactoryPresent() { - return STREAM_FACTORY_PRESENT; - } - - public MongoStreamFactoryFactorySettingsConfigurer(Builder settingsBuilder) { - this.settingsBuilder = settingsBuilder; - } - - void setStreamFactory(Object streamFactory) { - - if (MongoClientVersion.isVersion5orNewer() && isStreamFactoryPresent()) { - logger.warn("StreamFactoryFactory is no longer available. Use TransportSettings instead."); - return; - } - - try { - Class streamFactoryType = ClassUtils.forName(STREAM_FACTORY_NAME, streamFactory.getClass().getClassLoader()); - - if (!ClassUtils.isAssignable(streamFactoryType, streamFactory.getClass())) { - throw new IllegalArgumentException("Expected %s but found %s".formatted(streamFactoryType, streamFactory)); - } - - Method setter = ReflectionUtils.findMethod(settingsBuilder.getClass(), "streamFactoryFactory", - streamFactoryType); - if (setter != null) { - ReflectionUtils.invokeMethod(setter, settingsBuilder, streamFactoryType.cast(streamFactory)); - } - } catch (ReflectiveOperationException e) { - throw new IllegalArgumentException("Cannot set StreamFactoryFactory for %s".formatted(settingsBuilder), e); - } - } - } - -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java index 326a5c1e88..7fcc1383d5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java @@ -15,12 +15,14 @@ */ package org.springframework.data.mongodb.util; +import java.util.Collections; import java.util.HashMap; - -import org.springframework.lang.Nullable; +import java.util.Map; import com.mongodb.MongoException; +import org.jspecify.annotations.Nullable; + /** * {@link MongoDbErrorCodes} holds MongoDB specific error codes outlined in {@literal mongo/base/error_codes.yml}. * @@ -128,7 +130,9 @@ public final class MongoDbErrorCodes { clientSessionCodes.put(263, "OperationNotSupportedInTransaction"); clientSessionCodes.put(264, "TooManyLogicalSessions"); - errorCodes = new HashMap<>( + transactionCodes = new HashMap<>(0); + + errorCodes = new HashMap<>( dataAccessResourceFailureCodes.size() + dataIntegrityViolationCodes.size() + duplicateKeyCodes.size() + invalidDataAccessApiUsageException.size() + permissionDeniedCodes.size() + clientSessionCodes.size(), 1f); @@ -140,8 +144,7 @@ public final class MongoDbErrorCodes { errorCodes.putAll(clientSessionCodes); } - @Nullable - public static String getErrorDescription(@Nullable Integer errorCode) { + public static @Nullable String getErrorDescription(@Nullable Integer errorCode) { return errorCode == null ? null : errorCodes.get(errorCode); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/RegexFlags.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/RegexFlags.java index 23c96f9e46..8b0f4b83ce 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/RegexFlags.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/RegexFlags.java @@ -17,7 +17,7 @@ import java.util.regex.Pattern; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Utility to translate {@link Pattern#flags() regex flags} to MongoDB regex options and vice versa. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/SpringJsonWriter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/SpringJsonWriter.java new file mode 100644 index 0000000000..07eab92a01 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/SpringJsonWriter.java @@ -0,0 +1,487 @@ +/* + * 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.mongodb.util; + +import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Base64; + +import org.bson.BsonBinary; +import org.bson.BsonDbPointer; +import org.bson.BsonReader; +import org.bson.BsonRegularExpression; +import org.bson.BsonTimestamp; +import org.bson.BsonWriter; +import org.bson.types.Decimal128; +import org.bson.types.ObjectId; +import org.jspecify.annotations.NullUnmarked; +import org.springframework.util.StringUtils; + +/** + * Internal {@link BsonWriter} implementation that allows to render {@link #writePlaceholder(String) placeholders} as + * {@code ?0}. + * + * @author Christoph Strobl + * @since 5.0 + */ +@NullUnmarked +class SpringJsonWriter implements BsonWriter { + + private final StringBuffer buffer; + + private enum JsonContextType { + TOP_LEVEL, DOCUMENT, ARRAY, + } + + private enum State { + INITIAL, NAME, VALUE, DONE + } + + private static class JsonContext { + + private final JsonContext parentContext; + private final JsonContextType contextType; + private boolean hasElements; + + JsonContext(final JsonContext parentContext, final JsonContextType contextType) { + this.parentContext = parentContext; + this.contextType = contextType; + } + + JsonContext nestedDocument() { + return new JsonContext(this, JsonContextType.DOCUMENT); + } + + JsonContext nestedArray() { + return new JsonContext(this, JsonContextType.ARRAY); + } + } + + private JsonContext context = new JsonContext(null, JsonContextType.TOP_LEVEL); + private State state = State.INITIAL; + + public SpringJsonWriter(StringBuffer buffer) { + this.buffer = buffer; + } + + @Override + public void flush() {} + + @Override + public void writeBinaryData(BsonBinary binary) { + + preWriteValue(); + writeStartDocument(); + + writeName("$binary"); + + writeStartDocument(); + writeName("base64"); + writeString(Base64.getEncoder().encodeToString(binary.getData())); + writeName("subType"); + writeInt32(binary.getBsonType().getValue()); + writeEndDocument(); + + writeEndDocument(); + } + + @Override + public void writeBinaryData(String name, BsonBinary binary) { + + writeName(name); + writeBinaryData(binary); + } + + @Override + public void writeBoolean(boolean value) { + + preWriteValue(); + write(value ? "true" : "false"); + setNextState(); + } + + @Override + public void writeBoolean(String name, boolean value) { + + writeName(name); + writeBoolean(value); + } + + @Override + public void writeDateTime(long value) { + + // "$date": "2018-11-10T22:26:12.111Z" + writeStartDocument(); + writeName("$date"); + writeString(ZonedDateTime.ofInstant(Instant.ofEpochMilli(value), ZoneId.of("Z")).format(ISO_OFFSET_DATE_TIME)); + writeEndDocument(); + } + + @Override + public void writeDateTime(String name, long value) { + + writeName(name); + writeDateTime(value); + } + + @Override + public void writeDBPointer(BsonDbPointer value) { + + } + + @Override + public void writeDBPointer(String name, BsonDbPointer value) { + + } + + @Override // {"$numberDouble":"10.5"} + public void writeDouble(double value) { + + writeStartDocument(); + writeName("$numberDouble"); + writeString(Double.valueOf(value).toString()); + writeEndDocument(); + } + + @Override + public void writeDouble(String name, double value) { + + writeName(name); + writeDouble(value); + } + + @Override + public void writeEndArray() { + write("]"); + context = context.parentContext; + if (context.contextType == JsonContextType.TOP_LEVEL) { + state = State.DONE; + } else { + setNextState(); + } + } + + @Override + public void writeEndDocument() { + buffer.append("}"); + context = context.parentContext; + if (context.contextType == JsonContextType.TOP_LEVEL) { + state = State.DONE; + } else { + setNextState(); + } + } + + @Override + public void writeInt32(int value) { + + writeStartDocument(); + writeName("$numberInt"); + writeString(Integer.valueOf(value).toString()); + writeEndDocument(); + } + + @Override + public void writeInt32(String name, int value) { + + writeName(name); + writeInt32(value); + } + + @Override + public void writeInt64(long value) { + + writeStartDocument(); + writeName("$numberLong"); + writeString(Long.valueOf(value).toString()); + writeEndDocument(); + } + + @Override + public void writeInt64(String name, long value) { + + writeName(name); + writeInt64(value); + } + + @Override + public void writeDecimal128(Decimal128 value) { + + // { "$numberDecimal": "" } + writeStartDocument(); + writeName("$numberDecimal"); + writeString(value.toString()); + writeEndDocument(); + } + + @Override + public void writeDecimal128(String name, Decimal128 value) { + + writeName(name); + writeDecimal128(value); + } + + @Override + public void writeJavaScript(String code) { + + writeStartDocument(); + writeName("$code"); + writeString(code); + writeEndDocument(); + } + + @Override + public void writeJavaScript(String name, String code) { + + writeName(name); + writeJavaScript(code); + } + + @Override + public void writeJavaScriptWithScope(String code) { + + } + + @Override + public void writeJavaScriptWithScope(String name, String code) { + + } + + @Override + public void writeMaxKey() { + + writeStartDocument(); + writeName("$maxKey"); + buffer.append(1); + writeEndDocument(); + } + + @Override + public void writeMaxKey(String name) { + writeName(name); + writeMaxKey(); + } + + @Override + public void writeMinKey() { + + writeStartDocument(); + writeName("$minKey"); + buffer.append(1); + writeEndDocument(); + } + + @Override + public void writeMinKey(String name) { + writeName(name); + writeMinKey(); + } + + @Override + public void writeName(String name) { + if (context.hasElements) { + write(","); + } else { + context.hasElements = true; + } + + writeString(name); + buffer.append(":"); + state = State.VALUE; + } + + @Override + public void writeNull() { + buffer.append("null"); + } + + @Override + public void writeNull(String name) { + writeName(name); + writeNull(); + } + + @Override + public void writeObjectId(ObjectId objectId) { + writeStartDocument(); + writeName("$oid"); + writeString(objectId.toHexString()); + writeEndDocument(); + } + + @Override + public void writeObjectId(String name, ObjectId objectId) { + writeName(name); + writeObjectId(objectId); + } + + @Override + public void writeRegularExpression(BsonRegularExpression regularExpression) { + + writeStartDocument(); + writeName("$regex"); + + write("/"); + write(regularExpression.getPattern()); + write("/"); + + if (StringUtils.hasText(regularExpression.getOptions())) { + writeName("$options"); + writeString(regularExpression.getOptions()); + } + + writeEndDocument(); + } + + @Override + public void writeRegularExpression(String name, BsonRegularExpression regularExpression) { + writeName(name); + writeRegularExpression(regularExpression); + } + + @Override + public void writeStartArray() { + + preWriteValue(); + write("["); + context = context.nestedArray(); + } + + @Override + public void writeStartArray(String name) { + writeName(name); + writeStartArray(); + } + + @Override + public void writeStartDocument() { + + preWriteValue(); + write("{"); + context = context.nestedDocument(); + state = State.NAME; + } + + @Override + public void writeStartDocument(String name) { + writeName(name); + writeStartDocument(); + } + + @Override + public void writeString(String value) { + write("'"); + write(value); + write("'"); + } + + @Override + public void writeString(String name, String value) { + writeName(name); + writeString(value); + } + + @Override + public void writeSymbol(String value) { + + writeStartDocument(); + writeName("$symbol"); + writeString(value); + writeEndDocument(); + } + + @Override + public void writeSymbol(String name, String value) { + + writeName(name); + writeSymbol(value); + } + + @Override // {"$timestamp": {"t": , "i": }} + public void writeTimestamp(BsonTimestamp value) { + + preWriteValue(); + writeStartDocument(); + writeName("$timestamp"); + writeStartDocument(); + writeName("t"); + buffer.append(value.getTime()); + writeName("i"); + buffer.append(value.getInc()); + writeEndDocument(); + writeEndDocument(); + } + + @Override + public void writeTimestamp(String name, BsonTimestamp value) { + + writeName(name); + writeTimestamp(value); + } + + @Override + public void writeUndefined() { + + writeStartDocument(); + writeName("$undefined"); + writeBoolean(true); + writeEndDocument(); + } + + @Override + public void writeUndefined(String name) { + + writeName(name); + writeUndefined(); + } + + @Override + public void pipe(BsonReader reader) { + + } + + /** + * @param placeholder + */ + public void writePlaceholder(String placeholder) { + write(placeholder); + } + + private void write(String str) { + buffer.append(str); + } + + private void preWriteValue() { + + if (context.contextType == JsonContextType.ARRAY) { + if (context.hasElements) { + write(","); + } + } + context.hasElements = true; + } + + private void setNextState() { + if (context.contextType == JsonContextType.ARRAY) { + state = State.VALUE; + } else { + state = State.NAME; + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/aggregation/TestAggregationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/aggregation/TestAggregationContext.java index 344244717e..950f9ec797 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/aggregation/TestAggregationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/aggregation/TestAggregationContext.java @@ -17,6 +17,7 @@ import org.bson.Document; import org.bson.codecs.configuration.CodecRegistry; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; @@ -27,7 +28,6 @@ import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.lang.Nullable; /** * @author Christoph Strobl diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/encryption/EncryptionUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/encryption/EncryptionUtils.java index 9dd3f1d8fb..be9a2e1cff 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/encryption/EncryptionUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/encryption/EncryptionUtils.java @@ -22,10 +22,10 @@ import org.bson.BsonBinary; import org.bson.BsonBinarySubType; import org.bson.types.Binary; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.util.spel.ExpressionUtils; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -48,8 +48,7 @@ public final class EncryptionUtils { * @return can be {@literal null}. * @throws IllegalArgumentException if one of the required arguments is {@literal null}. */ - @Nullable - public static Object resolveKeyId(String value, Supplier evaluationContext) { + public static @Nullable Object resolveKeyId(String value, Supplier evaluationContext) { Assert.notNull(value, "Value must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/DateTimeFormatter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/DateTimeFormatter.java index b5c26755cf..3961fafc21 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/DateTimeFormatter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/DateTimeFormatter.java @@ -23,6 +23,8 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; +import org.jspecify.annotations.NullUnmarked; + /** * DateTimeFormatter implementation borrowed from MongoDB @@ -33,6 +35,7 @@ * @author Ross Lawley * @since 2.2 */ +@NullUnmarked class DateTimeFormatter { private static final int DATE_STRING_LENGTH = "1970-01-01".length(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/EvaluationContextExpressionEvaluator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/EvaluationContextExpressionEvaluator.java index 6c31a9721f..57fecd284c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/EvaluationContextExpressionEvaluator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/EvaluationContextExpressionEvaluator.java @@ -18,12 +18,12 @@ import java.util.Collections; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; /** * @author Christoph Strobl @@ -40,9 +40,8 @@ class EvaluationContextExpressionEvaluator implements ValueExpressionEvaluator { this.expressionParser = expressionParser; } - @Nullable @Override - public T evaluate(String expression) { + public @Nullable T evaluate(String expression) { return evaluateExpression(expression, Collections.emptyMap()); } @@ -55,7 +54,7 @@ Expression getParsedExpression(String expressionString) { } @SuppressWarnings("unchecked") - T evaluateExpression(String expressionString, Map variables) { + @Nullable T evaluateExpression(String expressionString, Map variables) { Expression expression = getParsedExpression(expressionString); EvaluationContext ctx = getEvaluationContext(expressionString); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonBuffer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonBuffer.java index 4b4b497dae..dcb9a3ff13 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonBuffer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonBuffer.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.util.json; import org.bson.json.JsonParseException; +import org.jspecify.annotations.NullUnmarked; /** * JsonBuffer implementation borrowed from @@ -32,6 +33,7 @@ * @author Christoph Strobl * @since 2.2 */ +@NullUnmarked class JsonScanner { private final JsonBuffer buffer; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonToken.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonToken.java index 293736123e..e73d57774b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonToken.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonToken.java @@ -20,6 +20,7 @@ import org.bson.BsonDouble; import org.bson.json.JsonParseException; import org.bson.types.Decimal128; +import org.jspecify.annotations.NullUnmarked; /** * JsonToken implementation borrowed from @@ -44,29 +42,6 @@ public class ParameterBindingContext { private final ValueProvider valueProvider; private final ValueExpressionEvaluator expressionEvaluator; - /** - * @param valueProvider - * @param expressionParser - * @param evaluationContext - * @deprecated since 4.4.0, use {@link #ParameterBindingContext(ValueProvider, ExpressionParser, Supplier)} instead. - */ - @Deprecated(since = "4.4.0") - public ParameterBindingContext(ValueProvider valueProvider, SpelExpressionParser expressionParser, - EvaluationContext evaluationContext) { - this(valueProvider, expressionParser, () -> evaluationContext); - } - - /** - * @param valueProvider - * @param expressionEvaluator - * @since 3.1 - * @deprecated since 4.4.0, use {@link #ParameterBindingContext(ValueProvider, ValueExpressionEvaluator)} instead. - */ - @Deprecated(since = "4.4.0") - public ParameterBindingContext(ValueProvider valueProvider, SpELExpressionEvaluator expressionEvaluator) { - this(valueProvider, (ValueExpressionEvaluator) expressionEvaluator); - } - /** * @param valueProvider * @param expressionParser @@ -153,18 +128,15 @@ public static ParameterBindingContext forExpressions(ValueProvider valueProvider return new ParameterBindingContext(valueProvider, expressionEvaluator); } - @Nullable - public Object bindableValueForIndex(int index) { + public @Nullable Object bindableValueForIndex(int index) { return valueProvider.getBindableValue(index); } - @Nullable - public Object evaluateExpression(String expressionString) { + public @Nullable Object evaluateExpression(String expressionString) { return expressionEvaluator.evaluate(expressionString); } - @Nullable - public Object evaluateExpression(String expressionString, Map variables) { + public @Nullable Object evaluateExpression(String expressionString, Map variables) { if (expressionEvaluator instanceof EvaluationContextExpressionEvaluator expressionEvaluator) { return expressionEvaluator.evaluateExpression(expressionString, variables); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java index ffa226ab69..8138f397a6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java @@ -40,14 +40,14 @@ import org.bson.codecs.*; import org.bson.codecs.configuration.CodecRegistry; import org.bson.json.JsonParseException; - +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.spel.ExpressionDependencies; import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; import org.springframework.util.NumberUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -66,6 +66,7 @@ * @author Rocco Lagrotteria * @since 2.2 */ +@NullUnmarked public class ParameterBindingDocumentCodec implements CollectibleCodec { private static final String ID_FIELD_NAME = FieldName.ID.name(); @@ -170,7 +171,7 @@ public void encode(final BsonWriter writer, final Document document, final Encod public Document decode(@Nullable String json, Object[] values) { return decode(json, new ParameterBindingContext((index) -> values[index], new SpelExpressionParser(), - EvaluationContextProvider.DEFAULT.getEvaluationContext(values))); + () -> EvaluationContextProvider.DEFAULT.getEvaluationContext(values))); } public Document decode(@Nullable String json, ParameterBindingContext bindingContext) { @@ -396,9 +397,8 @@ static class DependencyCapturingExpressionEvaluator implements ValueExpressionEv this.expressionParser = expressionParser; } - @Nullable @Override - public T evaluate(String expression) { + public @Nullable T evaluate(String expression) { dependencies.add(expressionParser.parse(expression).getExpressionDependencies()); return (T) PLACEHOLDER; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java index 8dd42e2427..c1e519e2f5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java @@ -39,10 +39,11 @@ import org.bson.types.MaxKey; import org.bson.types.MinKey; import org.bson.types.ObjectId; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.NumberUtils; import org.springframework.util.ObjectUtils; @@ -62,6 +63,7 @@ * @author Rocco Lagrotteria * @since 2.2 */ +@NullUnmarked public class ParameterBindingJsonReader extends AbstractBsonReader { private static final Pattern ENTIRE_QUERY_BINDING_PATTERN = Pattern.compile("^\\?(\\d+)$|^[\\?:][#$]\\{.*\\}$"); @@ -533,13 +535,11 @@ private BsonType bsonTypeForValue(Object value) { return BsonType.UNDEFINED; } - @Nullable - private Object evaluateExpression(String expressionString) { + private @Nullable Object evaluateExpression(String expressionString) { return bindingContext.evaluateExpression(expressionString, Collections.emptyMap()); } - @Nullable - private Object evaluateExpression(String expressionString, Map variables) { + private @Nullable Object evaluateExpression(String expressionString, Map variables) { return bindingContext.evaluateExpression(expressionString, variables); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ValueProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ValueProvider.java index 8f1d23885d..2ce22214fb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ValueProvider.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ValueProvider.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.util.json; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A value provider to retrieve bindable values by their parameter index. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/package-info.java index 8a86b3522b..60e5e8c609 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/package-info.java @@ -1,5 +1,5 @@ /** * MongoDB driver-specific utility classes for Json conversion. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.util.json; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/package-info.java index 7caec410f5..a697bb7000 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/package-info.java @@ -2,5 +2,5 @@ * MongoDB driver-specific utility classes for {@link org.bson.conversions.Bson} and {@link com.mongodb.DBObject} * interaction. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.util; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/spel/ExpressionUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/spel/ExpressionUtils.java index 9fa66b3b2b..796f618906 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/spel/ExpressionUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/spel/ExpressionUtils.java @@ -17,12 +17,12 @@ import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.ParserContext; import org.springframework.expression.common.LiteralExpression; import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -42,8 +42,7 @@ public final class ExpressionUtils { * @param potentialExpression can be {@literal null} * @return can be {@literal null}. */ - @Nullable - public static Expression detectExpression(@Nullable String potentialExpression) { + public static @Nullable Expression detectExpression(@Nullable String potentialExpression) { if (!StringUtils.hasText(potentialExpression)) { return null; @@ -53,8 +52,7 @@ public static Expression detectExpression(@Nullable String potentialExpression) return expression instanceof LiteralExpression ? null : expression; } - @Nullable - public static Object evaluate(String value, Supplier evaluationContext) { + public static @Nullable Object evaluate(String value, Supplier evaluationContext) { Expression expression = detectExpression(value); if (expression == null) { diff --git a/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensions.kt b/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensions.kt index d132482f65..d7784a7768 100644 --- a/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensions.kt +++ b/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensions.kt @@ -142,7 +142,7 @@ fun Update.pull(key: KProperty, value: Any) = * @since 4.4 * @see Update.pullAll */ -fun Update.pullAll(key: KProperty>, values: Array) = +fun Update.pullAll(key: KProperty>, values: Array) = pullAll(key.toDotPath(), values) /** diff --git a/spring-data-mongodb/src/main/resources/META-INF/spring.schemas b/spring-data-mongodb/src/main/resources/META-INF/spring.schemas index 57920f7449..c6c28dbab1 100644 --- a/spring-data-mongodb/src/main/resources/META-INF/spring.schemas +++ b/spring-data-mongodb/src/main/resources/META-INF/spring.schemas @@ -13,7 +13,8 @@ http\://www.springframework.org/schema/data/mongo/spring-mongo-2.2.xsd=org/sprin http\://www.springframework.org/schema/data/mongo/spring-mongo-3.0.xsd=org/springframework/data/mongodb/config/spring-mongo-3.0.xsd http\://www.springframework.org/schema/data/mongo/spring-mongo-3.3.xsd=org/springframework/data/mongodb/config/spring-mongo-3.3.xsd http\://www.springframework.org/schema/data/mongo/spring-mongo-4.0.xsd=org/springframework/data/mongodb/config/spring-mongo-4.0.xsd -http\://www.springframework.org/schema/data/mongo/spring-mongo.xsd=org/springframework/data/mongodb/config/spring-mongo-4.0.xsd +http\://www.springframework.org/schema/data/mongo/spring-mongo-5.0.xsd=org/springframework/data/mongodb/config/spring-mongo-5.0.xsd +http\://www.springframework.org/schema/data/mongo/spring-mongo.xsd=org/springframework/data/mongodb/config/spring-mongo-5.0.xsd https\://www.springframework.org/schema/data/mongo/spring-mongo-1.0.xsd=org/springframework/data/mongodb/config/spring-mongo-1.0.xsd https\://www.springframework.org/schema/data/mongo/spring-mongo-1.1.xsd=org/springframework/data/mongodb/config/spring-mongo-1.1.xsd https\://www.springframework.org/schema/data/mongo/spring-mongo-1.2.xsd=org/springframework/data/mongodb/config/spring-mongo-1.2.xsd @@ -29,4 +30,5 @@ https\://www.springframework.org/schema/data/mongo/spring-mongo-2.2.xsd=org/spri https\://www.springframework.org/schema/data/mongo/spring-mongo-3.0.xsd=org/springframework/data/mongodb/config/spring-mongo-3.0.xsd https\://www.springframework.org/schema/data/mongo/spring-mongo-3.3.xsd=org/springframework/data/mongodb/config/spring-mongo-3.3.xsd https\://www.springframework.org/schema/data/mongo/spring-mongo-4.0.xsd=org/springframework/data/mongodb/config/spring-mongo-4.0.xsd -https\://www.springframework.org/schema/data/mongo/spring-mongo.xsd=org/springframework/data/mongodb/config/spring-mongo-4.0.xsd +https\://www.springframework.org/schema/data/mongo/spring-mongo-5.0.xsd=org/springframework/data/mongodb/config/spring-mongo-5.0.xsd +https\://www.springframework.org/schema/data/mongo/spring-mongo.xsd=org/springframework/data/mongodb/config/spring-mongo-5.0.xsd diff --git a/spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-5.0.xsd b/spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-5.0.xsd new file mode 100644 index 0000000000..5fae630b6b --- /dev/null +++ b/spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-5.0.xsd @@ -0,0 +1,935 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The WriteConcern that will be the default value used when asking + the MongoDatabaseFactory for a DB object + + + + + + + + + + + + + + The reference to a MongoTemplate. Will default to 'mongoTemplate'. + + + + + + + Enables creation of indexes for queries that get derived from the + method name + and thus reference domain class properties. Defaults to false. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The reference to a MongoDatabaseFactory. + + + + + + + + + + + + The reference to a MongoTypeMapper to be used by this + MappingMongoConverter. + + + + + + + The reference to a MappingContext. Will default to + 'mappingContext'. + + + + + + + Disables JSR-303 validation on MongoDB documents before they are + saved. By default it is set to false. + + + + + + + + + + Enables abbreviating the field names for domain class properties + to the + first character of their camel case names, e.g. fooBar -> fb. + Defaults to false. + + + + + + + + + + The reference to a FieldNamingStrategy. + + + + + + + Enable/Disable index creation for annotated properties/entities. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A reference to a custom converter. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The reference to a MongoDatabaseFactory. + + + + + + + + + + + + The WriteConcern that will be the default value used when asking + the MongoDatabaseFactory for a DB object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The reference to a MongoDatabaseFactory. + + + + + + + + + + + + + + + + diff --git a/spring-data-mongodb/src/test/java/example/aot/User.java b/spring-data-mongodb/src/test/java/example/aot/User.java new file mode 100644 index 0000000000..06022c0a55 --- /dev/null +++ b/spring-data-mongodb/src/test/java/example/aot/User.java @@ -0,0 +1,94 @@ +/* + * 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 example.aot; + +import java.time.Instant; + +import org.springframework.data.mongodb.core.mapping.Field; + +/** + * @author Christoph Strobl + */ +public class User { + + String id; + + String username; + + @Field("first_name") String firstname; + + @Field("last_name") String lastname; + + Instant registrationDate; + Instant lastSeen; + Long visits; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public Instant getRegistrationDate() { + return registrationDate; + } + + public void setRegistrationDate(Instant registrationDate) { + this.registrationDate = registrationDate; + } + + public Instant getLastSeen() { + return lastSeen; + } + + public void setLastSeen(Instant lastSeen) { + this.lastSeen = lastSeen; + } + + public Long getVisits() { + return visits; + } + + public void setVisits(Long visits) { + this.visits = visits; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/monitor/Resumeable.java b/spring-data-mongodb/src/test/java/example/aot/UserProjection.java similarity index 73% rename from spring-data-mongodb/src/test/java/org/springframework/data/mongodb/monitor/Resumeable.java rename to spring-data-mongodb/src/test/java/example/aot/UserProjection.java index 1fdbb1f188..e59598d3a9 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/monitor/Resumeable.java +++ b/spring-data-mongodb/src/test/java/example/aot/UserProjection.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2025 the original author or authors. + * 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. @@ -13,15 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.mongodb.monitor; +package example.aot; -import java.util.function.Supplier; +import java.time.Instant; /** * @author Christoph Strobl - * @since 2018/01 */ -interface Resumeable { +public interface UserProjection { - void resumeAt(Supplier token); + String getUsername(); + + Instant getLastSeen(); } diff --git a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java new file mode 100644 index 0000000000..5eb9fed686 --- /dev/null +++ b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java @@ -0,0 +1,290 @@ +/* + * 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 example.aot; + +import java.time.Instant; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import org.springframework.data.annotation.Id; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Window; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.repository.Aggregation; +import org.springframework.data.mongodb.repository.Hint; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.data.mongodb.repository.ReadPreference; +import org.springframework.data.mongodb.repository.Update; +import org.springframework.data.repository.CrudRepository; + +/** + * @author Christoph Strobl + */ +public interface UserRepository extends CrudRepository { + + /* Derived Queries */ + + List findUserNoArgumentsBy(); + + User findOneByUsername(String username); + + Optional findOptionalOneByUsername(String username); + + Long countUsersByLastname(String lastname); + + int countUsersAsIntByLastname(String lastname); + + Boolean existsUserByLastname(String lastname); + + List findByLastnameStartingWith(String lastname); + + List findByLastnameEndsWith(String postfix); + + List findByFirstnameLike(String firstname); + + List findByFirstnameNotLike(String firstname); + + List findByUsernameIn(Collection usernames); + + List findByUsernameNotIn(Collection usernames); + + List findByFirstnameAndLastname(String firstname, String lastname); + + List findByFirstnameOrLastname(String firstname, String lastname); + + List findByVisitsBetween(long from, long to); + + List findByLastSeenGreaterThan(Instant time); + + List findByVisitsExists(boolean exists); + + List findByLastnameNot(String lastname); + + List findTop2ByLastnameStartingWith(String lastname); + + List findByLastnameStartingWithOrderByUsername(String lastname); + + List findByLastnameStartingWith(String lastname, Limit limit); + + List findByLastnameStartingWith(String lastname, Sort sort); + + List findByLastnameStartingWith(String lastname, Sort sort, Limit limit); + + List findByLastnameStartingWith(String lastname, Pageable page); + + Page findPageOfUsersByLastnameStartingWith(String lastname, Pageable page); + + Slice findSliceOfUserByLastnameStartingWith(String lastname, Pageable page); + + Stream streamByLastnameStartingWith(String lastname, Sort sort, Limit limit); + + Window findTop2WindowByLastnameStartingWithOrderByUsername(String lastname, ScrollPosition scrollPosition); + + // TODO: GeoQueries + // TODO: TextSearch + + /* Annotated Queries */ + + @Query("{ 'username' : ?0 }") + User findAnnotatedQueryByUsername(String username); + + @Query(value = "{ 'lastname' : { '$regex' : '^?0' } }", count = true) + Long countAnnotatedQueryByLastname(String lastname); + + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + List findAnnotatedQueryByLastname(String lastname); + + @Query(""" + { + 'lastname' : { + '$regex' : '^?0' + } + }""") + List findAnnotatedMultilineQueryByLastname(String username); + + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + List findAnnotatedQueryByLastname(String lastname, Limit limit); + + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + List findAnnotatedQueryByLastname(String lastname, Sort sort); + + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + List findAnnotatedQueryByLastname(String lastname, Limit limit, Sort sort); + + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + List findAnnotatedQueryByLastname(String lastname, Pageable pageable); + + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + Page findAnnotatedQueryPageOfUsersByLastname(String lastname, Pageable pageable); + + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + Slice findAnnotatedQuerySliceOfUsersByLastname(String lastname, Pageable pageable); + + /* deletes */ + + User deleteByUsername(String username); + + @Query(value = "{ 'username' : ?0 }", delete = true) + User deleteAnnotatedQueryByUsername(String username); + + Long deleteByLastnameStartingWith(String lastname); + + @Query(value = "{ 'lastname' : { '$regex' : '^?0' } }", delete = true) + Long deleteAnnotatedQueryByLastnameStartingWith(String lastname); + + List deleteUsersByLastnameStartingWith(String lastname); + + @Query(value = "{ 'lastname' : { '$regex' : '^?0' } }", delete = true) + List deleteUsersAnnotatedQueryByLastnameStartingWith(String lastname); + + /* Updates */ + + @Update("{ '$inc' : { 'visits' : ?1 } }") + int findUserAndIncrementVisitsByLastname(String lastname, int increment); + + @Query("{ 'lastname' : ?0 }") + @Update("{ '$inc' : { 'visits' : ?1 } }") + int updateAllByLastname(String lastname, int increment); + + @Update(pipeline = { "{ '$set' : { 'visits' : { '$ifNull' : [ {'$add' : [ '$visits', ?1 ] }, ?1 ] } } }" }) + void findAndIncrementVisitsViaPipelineByLastname(String lastname, int increment); + + /* Derived With Annotated Options */ + + @Query(sort = "{ 'username' : 1 }") + List findWithAnnotatedSortByLastnameStartingWith(String lastname); + + @Query(fields = "{ 'username' : 1 }") + List findWithAnnotatedFieldsProjectionByLastnameStartingWith(String lastname); + + @ReadPreference("no-such-read-preference") + User findWithReadPreferenceByUsername(String username); + + /* Projecting Queries */ + + List findUserProjectionByLastnameStartingWith(String lastname); + + Page findUserProjectionByLastnameStartingWith(String lastname, Pageable page); + + Page findUserProjectionByLastnameStartingWith(String lastname, Pageable page, Class projectionType); + + /* Aggregations */ + + @Aggregation(pipeline = { // + "{ '$match' : { 'last_name' : { '$ne' : null } } }", // + "{ '$project': { '_id' : '$last_name' } }" }) + List findAllLastnames(); + + @Aggregation(pipeline = { // + "{ '$match' : { 'last_name' : { '$ne' : null } } }", // + "{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" }) + List groupByLastnameAnd(String property); + + @Aggregation(pipeline = { // + "{ '$match' : { 'last_name' : { '$ne' : null } } }", // + "{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" }) + List groupByLastnameAnd(String property, Pageable pageable); + + @Aggregation(pipeline = { // + "{ '$match' : { 'last_name' : { '$ne' : null } } }", // + "{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" }) + Slice groupByLastnameAndReturnPage(String property, Pageable pageable); + + @Aggregation(pipeline = { // + "{ '$match' : { 'last_name' : { '$ne' : null } } }", // + "{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" }) + AggregationResults groupByLastnameAndAsAggregationResults(String property); + + @Aggregation(pipeline = { // + "{ '$match' : { 'last_name' : { '$ne' : null } } }", // + "{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" }) + Stream streamGroupByLastnameAndAsAggregationResults(String property); + + @Aggregation(pipeline = { // + "{ '$match' : { 'posts' : { '$ne' : null } } }", // + "{ '$project': { 'nrPosts' : {'$size': '$posts' } } }", // + "{ '$group' : { '_id' : null, 'total' : { $sum: '$nrPosts' } } }" }) + int sumPosts(); + + @Hint("ln-idx") + @Aggregation(pipeline = { // + "{ '$match' : { 'last_name' : { '$ne' : null } } }", // + "{ '$project': { '_id' : '$last_name' } }" }) + List findAllLastnamesUsingIndex(); + + @ReadPreference("no-such-read-preference") + @Aggregation(pipeline = { // + "{ '$match' : { 'last_name' : { '$ne' : null } } }", // + "{ '$project': { '_id' : '$last_name' } }" }) + List findAllLastnamesWithReadPreference(); + + @Aggregation(pipeline = { // + "{ '$match' : { 'last_name' : { '$ne' : null } } }", // + "{ '$project': { '_id' : '$last_name' } }" }, collation = "no_collation") + List findAllLastnamesWithCollation(); + + class UserAggregate { + + @Id // + private final String lastname; + private final Set names; + + public UserAggregate(String lastname, Collection names) { + this.lastname = lastname; + this.names = new HashSet<>(names); + } + + public String getLastname() { + return this.lastname; + } + + public Set getNames() { + return this.names; + } + + @Override + public String toString() { + return "UserAggregate{" + "lastname='" + lastname + '\'' + ", names=" + names + '}'; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UserAggregate that = (UserAggregate) o; + return Objects.equals(lastname, that.lastname) && names.equals(that.names); + } + + @Override + public int hashCode() { + return Objects.hash(lastname, names); + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/CapturingTransactionOptionsResolver.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/CapturingTransactionOptionsResolver.java index 0448ad936c..c05122873c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/CapturingTransactionOptionsResolver.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/CapturingTransactionOptionsResolver.java @@ -21,7 +21,7 @@ import org.assertj.core.api.Assertions; import org.assertj.core.api.ListAssert; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.CollectionUtils; /** @@ -36,9 +36,8 @@ public CapturingTransactionOptionsResolver(MongoTransactionOptionsResolver deleg this.delegateResolver = delegateResolver; } - @Nullable @Override - public String getLabelPrefix() { + public @Nullable String getLabelPrefix() { return delegateResolver.getLabelPrefix(); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionOptionsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionOptionsUnitTests.java index 44692348a0..d89edc6206 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionOptionsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionOptionsUnitTests.java @@ -20,8 +20,8 @@ import java.time.Duration; import java.util.concurrent.TimeUnit; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; -import org.springframework.lang.Nullable; import com.mongodb.ReadConcern; import com.mongodb.ReadPreference; @@ -90,27 +90,23 @@ void testEquals() { assertThat(MongoTransactionOptions.NONE) // .isSameAs(MongoTransactionOptions.NONE) // .isNotEqualTo(new MongoTransactionOptions() { - @Nullable @Override - public Duration getMaxCommitTime() { + public @Nullable Duration getMaxCommitTime() { return null; } - @Nullable @Override - public ReadConcern getReadConcern() { + public @Nullable ReadConcern getReadConcern() { return null; } - @Nullable @Override - public ReadPreference getReadPreference() { + public @Nullable ReadPreference getReadPreference() { return null; } - @Nullable @Override - public WriteConcern getWriteConcern() { + public @Nullable WriteConcern getWriteConcern() { return null; } }); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/DemoRepo.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/DemoRepo.java new file mode 100644 index 0000000000..41759e68c7 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/DemoRepo.java @@ -0,0 +1,46 @@ +/* + * 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.mongodb.aot.generated; + +import java.util.List; + +import example.aot.User; +import org.springframework.data.mongodb.BindableMongoExpression; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.repository.query.StringBasedMongoQuery; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public class DemoRepo { + + + MongoOperations operations; + + List method1(String username) { + + BindableMongoExpression filter = new BindableMongoExpression("{ 'username', ?0 }", operations.getConverter(), new Object[]{username}); + Query query = new BasicQuery(filter.toDocument()); + + return operations.query(User.class) + .as(User.class) + .matching(query) + .all(); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollectionOptionsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollectionOptionsUnitTests.java index f2691275c3..9de0863cd2 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollectionOptionsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollectionOptionsUnitTests.java @@ -15,12 +15,24 @@ */ package org.springframework.data.mongodb.core; -import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.mongodb.core.CollectionOptions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.data.mongodb.core.CollectionOptions.EncryptedFieldsOptions; +import static org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions; +import static org.springframework.data.mongodb.core.CollectionOptions.emitChangedRevisions; +import static org.springframework.data.mongodb.core.CollectionOptions.empty; +import static org.springframework.data.mongodb.core.CollectionOptions.encryptedCollection; +import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.int32; +import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.queryable; +import java.util.List; + +import org.bson.BsonNull; import org.bson.Document; import org.junit.jupiter.api.Test; import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.core.schema.JsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.MongoJsonSchema; +import org.springframework.data.mongodb.core.schema.QueryCharacteristics; import org.springframework.data.mongodb.core.validation.Validator; /** @@ -76,4 +88,93 @@ void validatorEquals() { .isNotEqualTo(empty().validator(Validator.document(new Document("three", "four")))) .isNotEqualTo(empty().validator(Validator.document(new Document("one", "two"))).moderateValidation()); } + + @Test // GH-4185 + @SuppressWarnings("unchecked") + void queryableEncryptionOptionsFromSchemaRenderCorrectly() { + + MongoJsonSchema schema = MongoJsonSchema.builder() + .property(JsonSchemaProperty.object("spring") + .properties(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int32("data")), List.of()))) + .property(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("mongodb")), List.of())).build(); + + EncryptedFieldsOptions encryptionOptions = EncryptedFieldsOptions.fromSchema(schema); + + assertThat(encryptionOptions.toDocument().get("fields", List.class)).hasSize(2) + .contains(new Document("path", "mongodb").append("bsonType", "long").append("queries", List.of()) + .append("keyId", BsonNull.VALUE)) + .contains(new Document("path", "spring.data").append("bsonType", "int").append("queries", List.of()) + .append("keyId", BsonNull.VALUE)); + } + + @Test // GH-4185 + @SuppressWarnings("unchecked") + void queryableEncryptionPropertiesOverrideByPath() { + + CollectionOptions collectionOptions = encryptedCollection(options -> options // + .queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int32("spring"))) + .queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("data"))) + + // override first with data type long + .queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("spring")))); + + assertThat(collectionOptions.getEncryptedFieldsOptions()).map(EncryptedFieldsOptions::toDocument) + .hasValueSatisfying(it -> { + assertThat(it.get("fields", List.class)).hasSize(2).contains(new Document("path", "spring") + .append("bsonType", "long").append("queries", List.of()).append("keyId", BsonNull.VALUE)); + }); + } + + @Test // GH-4185 + @SuppressWarnings("unchecked") + void queryableEncryptionPropertiesOverridesPathFromSchema() { + + EncryptedFieldsOptions encryptionOptions = EncryptedFieldsOptions.fromSchema(MongoJsonSchema.builder() + .property(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int32("spring")), List.of())) + .property(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("data")), List.of())).build()); + + // override spring from schema with data type long + CollectionOptions collectionOptions = CollectionOptions.encryptedCollection( + encryptionOptions.queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("spring")))); + + assertThat(collectionOptions.getEncryptedFieldsOptions()).map(EncryptedFieldsOptions::toDocument) + .hasValueSatisfying(it -> { + assertThat(it.get("fields", List.class)).hasSize(2).contains(new Document("path", "spring") + .append("bsonType", "long").append("queries", List.of()).append("keyId", BsonNull.VALUE)); + }); + } + + @Test // GH-4185 + void encryptionOptionsAreImmutable() { + + EncryptedFieldsOptions source = EncryptedFieldsOptions + .fromProperties(List.of(queryable(int32("spring.data"), List.of(QueryCharacteristics.range().min(1))))); + + assertThat(source.queryable(queryable(int32("mongodb"), List.of(QueryCharacteristics.range().min(1))))) + .isNotSameAs(source).satisfies(it -> { + assertThat(it.toDocument().get("fields", List.class)).hasSize(2); + }); + + assertThat(source.toDocument().get("fields", List.class)).hasSize(1); + } + + @Test // GH-4185 + @SuppressWarnings("unchecked") + void queryableEncryptionPropertiesOverridesNestedPathFromSchema() { + + EncryptedFieldsOptions encryptionOptions = EncryptedFieldsOptions.fromSchema(MongoJsonSchema.builder() + .property(JsonSchemaProperty.object("spring") + .properties(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int32("data")), List.of()))) + .property(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("mongodb")), List.of())).build()); + + // override spring from schema with data type long + CollectionOptions collectionOptions = CollectionOptions.encryptedCollection( + encryptionOptions.queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("spring.data")))); + + assertThat(collectionOptions.getEncryptedFieldsOptions()).map(EncryptedFieldsOptions::toDocument) + .hasValueSatisfying(it -> { + assertThat(it.get("fields", List.class)).hasSize(2).contains(new Document("path", "spring.data") + .append("bsonType", "long").append("queries", List.of()).append("keyId", BsonNull.VALUE)); + }); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java index af4fac84b1..78a6e6b496 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java @@ -15,9 +15,9 @@ */ package org.springframework.data.mongodb.core; -import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.mongodb.core.index.PartialIndexFilter.*; -import static org.springframework.data.mongodb.core.query.Criteria.*; +import static org.springframework.data.mongodb.core.index.PartialIndexFilter.of; +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.test.util.Assertions.assertThat; import org.bson.BsonDocument; import org.bson.Document; @@ -79,7 +79,7 @@ public void shouldApplyPartialFilterCorrectly() { IndexDefinition id = new Index().named("partial-with-criteria").on("k3y", Direction.ASC) .partial(of(where("q-t-y").gte(10))); - indexOps.ensureIndex(id); + indexOps.createIndex(id); IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "partial-with-criteria"); assertThat(Document.parse(info.getPartialFilterExpression())) @@ -92,7 +92,7 @@ public void shouldApplyPartialFilterWithMappedPropertyCorrectly() { IndexDefinition id = new Index().named("partial-with-mapped-criteria").on("k3y", Direction.ASC) .partial(of(where("quantity").gte(10))); - template.indexOps(DefaultIndexOperationsIntegrationTestsSample.class).ensureIndex(id); + template.indexOps(DefaultIndexOperationsIntegrationTestsSample.class).createIndex(id); IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "partial-with-mapped-criteria"); assertThat(Document.parse(info.getPartialFilterExpression())) @@ -105,7 +105,7 @@ public void shouldApplyPartialDBOFilterCorrectly() { IndexDefinition id = new Index().named("partial-with-dbo").on("k3y", Direction.ASC) .partial(of(new org.bson.Document("qty", new org.bson.Document("$gte", 10)))); - indexOps.ensureIndex(id); + indexOps.createIndex(id); IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "partial-with-dbo"); assertThat(Document.parse(info.getPartialFilterExpression())) @@ -120,7 +120,7 @@ public void shouldFavorExplicitMappingHintViaClass() { indexOps = new DefaultIndexOperations(template, COLLECTION_NAME, MappingToSameCollection.class); - indexOps.ensureIndex(id); + indexOps.createIndex(id); IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "partial-with-inheritance"); assertThat(Document.parse(info.getPartialFilterExpression())) @@ -150,7 +150,7 @@ public void shouldCreateIndexWithCollationCorrectly() { new DefaultIndexOperations(template, COLLECTION_NAME, MappingToSameCollection.class); - indexOps.ensureIndex(id); + indexOps.createIndex(id); Document expected = new Document("locale", "de_AT") // .append("caseLevel", false) // @@ -179,7 +179,7 @@ void indexShouldNotBeHiddenByDefault() { IndexDefinition index = new Index().named("my-index").on("a", Direction.ASC); indexOps = new DefaultIndexOperations(template, COLLECTION_NAME, MappingToSameCollection.class); - indexOps.ensureIndex(index); + indexOps.createIndex(index); IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "my-index"); assertThat(info.isHidden()).isFalse(); @@ -191,7 +191,7 @@ void shouldCreateHiddenIndex() { IndexDefinition index = new Index().named("my-hidden-index").on("a", Direction.ASC).hidden(); indexOps = new DefaultIndexOperations(template, COLLECTION_NAME, MappingToSameCollection.class); - indexOps.ensureIndex(index); + indexOps.createIndex(index); IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "my-hidden-index"); assertThat(info.isHidden()).isTrue(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupportUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupportUnitTests.java index 05f0695839..80373562c8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupportUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupportUnitTests.java @@ -33,6 +33,7 @@ * Unit tests for {@link ExecutableAggregationOperationSupport}. * * @author Christoph Strobl + * @author Mark Paluch */ @ExtendWith(MockitoExtension.class) public class ExecutableAggregationOperationSupportUnitTests { @@ -72,7 +73,8 @@ void aggregateWithUntypedAggregationAndExplicitCollection() { opSupport.aggregateAndReturn(Person.class).inCollection("star-wars").by(newAggregation(project("foo"))).all(); ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); - verify(template).aggregate(any(Aggregation.class), eq("star-wars"), captor.capture()); + verify(template).doAggregate(any(Aggregation.class), eq("star-wars"), captor.capture(), + eq(QueryResultConverter.entity())); assertThat(captor.getValue()).isEqualTo(Person.class); } @@ -86,7 +88,8 @@ void aggregateWithUntypedAggregation() { ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); verify(template).getCollectionName(captor.capture()); - verify(template).aggregate(any(Aggregation.class), eq("person"), captor.capture()); + verify(template).doAggregate(any(Aggregation.class), eq("person"), captor.capture(), + eq(QueryResultConverter.entity())); assertThat(captor.getAllValues()).containsExactly(Person.class, Person.class); } @@ -101,7 +104,8 @@ void aggregateWithTypeAggregation() { ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); verify(template).getCollectionName(captor.capture()); - verify(template).aggregate(any(Aggregation.class), eq("person"), captor.capture()); + verify(template).doAggregate(any(Aggregation.class), eq("person"), captor.capture(), + eq(QueryResultConverter.entity())); assertThat(captor.getAllValues()).containsExactly(Person.class, Jedi.class); } @@ -112,7 +116,8 @@ void aggregateStreamWithUntypedAggregationAndExplicitCollection() { opSupport.aggregateAndReturn(Person.class).inCollection("star-wars").by(newAggregation(project("foo"))).stream(); ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); - verify(template).aggregateStream(any(Aggregation.class), eq("star-wars"), captor.capture()); + verify(template).doAggregateStream(any(Aggregation.class), eq("star-wars"), captor.capture(), + eq(QueryResultConverter.entity()), any()); assertThat(captor.getValue()).isEqualTo(Person.class); } @@ -126,7 +131,8 @@ void aggregateStreamWithUntypedAggregation() { ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); verify(template).getCollectionName(captor.capture()); - verify(template).aggregateStream(any(Aggregation.class), eq("person"), captor.capture()); + verify(template).doAggregateStream(any(Aggregation.class), eq("person"), captor.capture(), + eq(QueryResultConverter.entity()), any()); assertThat(captor.getAllValues()).containsExactly(Person.class, Person.class); } @@ -141,7 +147,8 @@ void aggregateStreamWithTypeAggregation() { ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); verify(template).getCollectionName(captor.capture()); - verify(template).aggregateStream(any(Aggregation.class), eq("person"), captor.capture()); + verify(template).doAggregateStream(any(Aggregation.class), eq("person"), captor.capture(), + eq(QueryResultConverter.entity()), any()); assertThat(captor.getAllValues()).containsExactly(Person.class, Jedi.class); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java index eac248e69a..835367990a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java @@ -21,7 +21,9 @@ import static org.springframework.data.mongodb.test.util.DirtiesStateExtension.*; import java.util.Date; +import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.stream.Stream; import org.bson.BsonString; @@ -170,6 +172,16 @@ void findAllByWithProjection() { .hasOnlyElementsOfType(Jedi.class).hasSize(1); } + @Test // GH-4949 + void findAllByWithConverter() { + + List> result = template.query(Person.class).as(Jedi.class) + .matching(query(where("firstname").is("luke"))).map((document, reader) -> Optional.of(reader.get())).all(); + + assertThat(result).hasOnlyElementsOfType(Optional.class).hasSize(1); + assertThat(result).extracting(Optional::get).hasOnlyElementsOfType(Jedi.class).hasSize(1); + } + @Test // DATAMONGO-1563 void findBy() { assertThat(template.query(Person.class).matching(query(where("firstname").is("luke"))).one()).contains(luke); @@ -260,6 +272,15 @@ void streamAllWithProjection() { } } + @Test // GH-4949 + void streamAllWithConverter() { + + try (Stream> stream = template.query(Person.class).as(Jedi.class) + .map((document, reader) -> Optional.of(reader.get())).stream()) { + assertThat(stream).extracting(Optional::get).hasOnlyElementsOfType(Jedi.class).hasSize(2); + } + } + @Test // DATAMONGO-1733 void streamAllReturningResultsAsClosedInterfaceProjection() { @@ -315,6 +336,20 @@ void findAllNearByWithCollectionAndProjection() { assertThat(results.getContent().get(0).getContent().getId()).isEqualTo("alderan"); } + @Test // GH-4949 + void findAllNearByWithConverter() { + + GeoResults> results = template.query(Object.class).inCollection(STAR_WARS_PLANETS).as(Human.class) + .near(NearQuery.near(-73.9667, 40.78).spherical(true)).map((document, reader) -> Optional.of(reader.get())) + .all(); + + assertThat(results.getContent()).hasSize(2); + assertThat(results.getContent().get(0).getDistance()).isNotNull(); + assertThat(results.getContent().get(0).getContent()).isInstanceOf(Optional.class); + assertThat(results.getContent().get(0).getContent().get()).isInstanceOf(Human.class); + assertThat(results.getContent().get(0).getContent().get().getId()).isEqualTo("alderan"); + } + @Test // DATAMONGO-1733 void findAllNearByReturningGeoResultContentAsClosedInterfaceProjection() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java index 621e2a0764..fe19672068 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Objects; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -108,6 +109,14 @@ void removeAndReturnAllMatching() { assertThat(result).containsExactly(han); } + @Test // GH-4949 + void removeAndReturnAllMatchingWithResultConverter() { + + List> result = template.remove(Person.class).matching(query(where("firstname").is("han"))).map((raw, converted) -> Optional.of(converted.get())).findAndRemove(); + + assertThat(result).containsExactly(Optional.of(han)); + } + @org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS) static class Person { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java index e7f50dab53..46732b1a29 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java @@ -185,6 +185,17 @@ void findAndModifyWithOptions() { assertThat(result.get()).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Han"); } + @Test // GH-4949 + void findAndModifyWithResultConverter() { + + Optional result = template.update(Person.class).matching(queryHan()) + .apply(new Update().set("firstname", "Han")).withOptions(FindAndModifyOptions.options().returnNew(true)) + .map((raw, converted) -> Optional.of(converted.get())) + .findAndModifyValue(); + + assertThat(result.get()).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Han"); + } + @Test // DATAMONGO-1563 void upsert() { @@ -282,6 +293,19 @@ void findAndReplaceWithProjection() { assertThat(result.getName()).isEqualTo(han.firstname); } + @Test // GH-4949 + void findAndReplaceWithResultConverter() { + + Person luke = new Person(); + luke.firstname = "Luke"; + + Optional result = template.update(Person.class).matching(queryHan()).replaceWith(luke).as(Jedi.class) // + .map((raw, converted) -> Optional.of(converted.get())) + .findAndReplaceValue(); + + assertThat(result.get()).isInstanceOf(Jedi.class).extracting(Jedi::getName).isEqualTo(han.firstname); + } + private Query queryHan() { return query(where("id").is(han.getId())); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreatorUnitTests.java index d18ed6f119..adaecad5da 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreatorUnitTests.java @@ -15,7 +15,8 @@ */ package org.springframework.data.mongodb.core; -import static org.springframework.data.mongodb.test.util.Assertions.*; +import static org.springframework.data.mongodb.test.util.Assertions.assertThat; +import static org.springframework.data.mongodb.test.util.Assertions.assertThatExceptionOfType; import java.util.Collections; import java.util.Date; @@ -38,6 +39,8 @@ import org.springframework.data.mongodb.core.mapping.FieldType; import org.springframework.data.mongodb.core.mapping.MongoId; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.Queryable; +import org.springframework.data.mongodb.core.mapping.RangeEncrypted; import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type; import org.springframework.data.mongodb.core.schema.JsonSchemaProperty; import org.springframework.data.mongodb.core.schema.MongoJsonSchema; @@ -282,6 +285,48 @@ void wrapEncryptedEntityTypeLikeProperty() { .containsEntry("properties.domainTypeValue", Document.parse("{'encrypt': {'bsonType': 'object' } }")); } + @Test // GH-4185 + void qeRangeEncryptedProperties() { + + MongoJsonSchema schema = MongoJsonSchemaCreator.create() // + .filter(MongoJsonSchemaCreator.encryptedOnly()) // filter non encrypted fields + .createSchemaFor(QueryableEncryptedRoot.class); + + String expectedForInt = """ + { 'encrypt' : { + 'algorithm' : 'Range', + 'bsonType' : 'int', + 'queries' : [ + { 'queryType' : 'range', 'contention' : { '$numberLong' : '0' }, 'max' : 200, 'min' : 0, 'sparsity' : 1, 'trimFactor' : 1 } + ] + }}"""; + + String expectedForRootLong = """ + { 'encrypt' : { + 'algorithm' : 'Range', + 'bsonType' : 'long', + 'queries' : [ + { 'queryType' : 'range', contention : { '$numberLong' : '0' }, 'sparsity' : 0 } + ] + }}"""; + + String expectedForNestedLong = """ + { 'encrypt' : { + 'algorithm' : 'Range', + 'bsonType' : 'long', + 'queries' : [ + { 'queryType' : 'range', contention : { '$numberLong' : '1' }, 'max' : { '$numberLong' : '1' }, 'min' : { '$numberLong' : '-1' }, 'sparsity' : 1, 'trimFactor' : 1 } + ] + }}"""; + + assertThat(schema.schemaDocument()) // + .doesNotContainKey("properties.unencrypted") // + .containsEntry("properties.encryptedInt", Document.parse(expectedForInt)) + .containsEntry("properties.encryptedLong", Document.parse(expectedForRootLong)) + .containsEntry("properties.nested.properties.encrypted_long", Document.parse(expectedForNestedLong)); + + } + // --> TYPES AND JSON // --> ENUM @@ -311,7 +356,8 @@ enum JustSomeEnum { " 'binaryDataProperty' : { 'bsonType' : 'binData' }," + // " 'collectionProperty' : { 'type' : 'array' }," + // " 'simpleTypeCollectionProperty' : { 'type' : 'array', 'items' : { 'type' : 'string' } }," + // - " 'complexTypeCollectionProperty' : { 'type' : 'array', 'items' : { 'type' : 'object', 'properties' : { 'field' : { 'type' : 'string'} } } }" + // + " 'complexTypeCollectionProperty' : { 'type' : 'array', 'items' : { 'type' : 'object', 'properties' : { 'field' : { 'type' : 'string'} } } }" + + // " 'enumTypeCollectionProperty' : { 'type' : 'array', 'items' : " + JUST_SOME_ENUM + " }" + // " 'mapProperty' : { 'type' : 'object' }," + // " 'objectProperty' : { 'type' : 'object' }," + // @@ -692,4 +738,28 @@ static class PropertyClashWithA { static class WithEncryptedEntityLikeProperty { @Encrypted SomeDomainType domainTypeValue; } + + static class QueryableEncryptedRoot { + + String unencrypted; + + @RangeEncrypted(contentionFactor = 0L, rangeOptions = "{ 'min': 0, 'max': 200, 'trimFactor': 1, 'sparsity': 1}") // + Integer encryptedInt; + + @Encrypted(algorithm = "Range") + @Queryable(contentionFactor = 0L, queryType = "range", queryAttributes = "{ 'sparsity': 0 }") // + Long encryptedLong; + + NestedRangeEncrypted nested; + + } + + static class NestedRangeEncrypted { + + @Field("encrypted_long") + @RangeEncrypted(contentionFactor = 1L, + rangeOptions = "{ 'min': { '$numberLong' : '-1' }, 'max': { '$numberLong' : '1' }, 'trimFactor': 1, 'sparsity': 1}") // + Long encryptedLong; + } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java index 9730e61e51..bdc151ec63 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.*; import org.bson.BsonDocument; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -31,7 +32,6 @@ import org.springframework.data.mongodb.ClientSessionException; import org.springframework.data.mongodb.MongoTransactionException; import org.springframework.data.mongodb.UncategorizedMongoDbException; -import org.springframework.lang.Nullable; import com.mongodb.MongoCursorNotFoundException; import com.mongodb.MongoException; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java index 51b3b005a5..9a6bbb4f29 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java @@ -29,6 +29,7 @@ import org.bson.Document; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -48,7 +49,6 @@ import org.springframework.data.mongodb.test.util.Client; import org.springframework.data.mongodb.test.util.MongoClientExtension; import org.springframework.data.mongodb.test.util.MongoTestTemplate; -import org.springframework.lang.Nullable; import com.mongodb.client.MongoClient; import com.mongodb.client.model.Filters; @@ -1936,9 +1936,8 @@ public String toString() { static class ReferencableConverter implements Converter> { - @Nullable @Override - public DocumentPointer convert(ReferenceAble source) { + public @Nullable DocumentPointer convert(ReferenceAble source) { return source::toReference; } } @@ -1947,9 +1946,8 @@ public DocumentPointer convert(ReferenceAble source) { static class DocumentToSimpleObjectRefWithReadingConverter implements Converter, SimpleObjectRefWithReadingConverter> { - @Nullable @Override - public SimpleObjectRefWithReadingConverter convert(DocumentPointer source) { + public @Nullable SimpleObjectRefWithReadingConverter convert(DocumentPointer source) { Document document = client.getDatabase(DB_NAME).getCollection("simple-object-ref") .find(Filters.eq("_id", source.getPointer().get("ref-key-from-custom-write-converter"))).first(); @@ -1961,9 +1959,8 @@ public SimpleObjectRefWithReadingConverter convert(DocumentPointer sou static class SimpleObjectRefWithReadingConverterToDocumentConverter implements Converter> { - @Nullable @Override - public DocumentPointer convert(SimpleObjectRefWithReadingConverter source) { + public @Nullable DocumentPointer convert(SimpleObjectRefWithReadingConverter source) { return () -> new Document("ref-key-from-custom-write-converter", source.getId()); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateScrollTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateScrollTests.java index 766929c732..772392f037 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateScrollTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateScrollTests.java @@ -26,6 +26,7 @@ import java.util.stream.Stream; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -47,7 +48,6 @@ import org.springframework.data.mongodb.test.util.Client; import org.springframework.data.mongodb.test.util.MongoClientExtension; import org.springframework.data.mongodb.test.util.MongoTestTemplate; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import com.mongodb.client.MongoClient; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java index 83d4e30cc5..5a006bebfe 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java @@ -34,7 +34,9 @@ import java.util.stream.IntStream; import java.util.stream.Stream; +import org.bson.Document; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -48,7 +50,7 @@ import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.annotation.Version; import org.springframework.data.auditing.IsNewAwareAuditingHandler; import org.springframework.data.domain.PageRequest; @@ -90,7 +92,6 @@ import org.springframework.data.mongodb.test.util.MongoTestTemplate; import org.springframework.data.mongodb.test.util.MongoTestUtils; import org.springframework.data.mongodb.test.util.MongoVersion; -import org.springframework.lang.Nullable; import org.springframework.test.annotation.DirtiesContext; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -3110,6 +3111,18 @@ public void generatesIdForInsertAll() { assertThat(jesse.getId()).isNotNull(); } + @Test // GH-4944 + public void insertAllShouldConvertIdToTargetTypeBeforeSave() { + + RawStringId walter = new RawStringId(); + walter.value = "walter"; + + RawStringId returned = template.insertAll(List.of(walter)).iterator().next(); + org.bson.Document document = template.execute(RawStringId.class, collection -> collection.find().first()); + + assertThat(returned.id).isEqualTo(document.get("_id")); + } + @Test // DATAMONGO-1208 public void takesSortIntoAccountWhenStreaming() { @@ -4469,7 +4482,7 @@ static class TestClass { LocalDateTime myDate; - @PersistenceConstructor + @PersistenceCreator TestClass(LocalDateTime myDate) { this.myDate = myDate; } @@ -4726,7 +4739,7 @@ static class DocumentWithLazyDBrefUsedInPresistenceConstructor { @org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) Document refToDocUsedInCtor; @org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) Document refToDocNotUsedInCtor; - @PersistenceConstructor + @PersistenceCreator public DocumentWithLazyDBrefUsedInPresistenceConstructor(Document refToDocUsedInCtor) { this.refToDocUsedInCtor = refToDocUsedInCtor; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java index 79a0bb1fcb..ef72548fac 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java @@ -39,6 +39,7 @@ import org.bson.Document; import org.bson.conversions.Bson; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -99,7 +100,6 @@ import org.springframework.data.mongodb.core.timeseries.Granularity; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; -import org.springframework.lang.Nullable; import org.springframework.mock.env.MockEnvironment; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.CollectionUtils; @@ -437,8 +437,8 @@ void findAllAndRemoveShouldRemoveDocumentsReturedByFindQuery() { verify(collection, times(1)).deleteMany(queryCaptor.capture(), any()); - Document idField = DocumentTestUtils.getAsDocument(queryCaptor.getValue(), "_id"); - assertThat((List) idField.get("$in")).containsExactly(Integer.valueOf(0), Integer.valueOf(1)); + List ors = DocumentTestUtils.getAsDBList(queryCaptor.getValue(), "$or"); + assertThat(ors).containsExactlyInAnyOrder(new Document("_id", 0), new Document("_id", 1)); } @Test // DATAMONGO-566 @@ -1156,7 +1156,7 @@ void countShouldApplyQueryHintAsIndexNameIfPresent() { void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() { template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class, - PersonProjection.class, CursorPreparer.NO_OP_PREPARER); + PersonProjection.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document("firstname", 1))); } @@ -1165,7 +1165,7 @@ void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() { void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() { template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document("bar", 1), Person.class, - PersonProjection.class, CursorPreparer.NO_OP_PREPARER); + PersonProjection.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document("bar", 1))); } @@ -1174,7 +1174,7 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() { void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() { template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class, - PersonSpELProjection.class, CursorPreparer.NO_OP_PREPARER); + PersonSpELProjection.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(BsonUtils.EMPTY_DOCUMENT)); } @@ -1183,7 +1183,7 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() { void appliesFieldsToDtoProjection() { template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class, - Jedi.class, CursorPreparer.NO_OP_PREPARER); + Jedi.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document("firstname", 1))); } @@ -1192,7 +1192,7 @@ void appliesFieldsToDtoProjection() { void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() { template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document("bar", 1), Person.class, - Jedi.class, CursorPreparer.NO_OP_PREPARER); + Jedi.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document("bar", 1))); } @@ -1201,7 +1201,7 @@ void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() { void doesNotApplyFieldsWhenTargetIsNotAProjection() { template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class, - Person.class, CursorPreparer.NO_OP_PREPARER); + Person.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(BsonUtils.EMPTY_DOCUMENT)); } @@ -1210,7 +1210,7 @@ void doesNotApplyFieldsWhenTargetIsNotAProjection() { void doesNotApplyFieldsWhenTargetExtendsDomainType() { template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class, - PersonExtended.class, CursorPreparer.NO_OP_PREPARER); + PersonExtended.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(BsonUtils.EMPTY_DOCUMENT)); } @@ -2908,8 +2908,7 @@ public List getValues() { return values; } - @Nullable - public T getValue() { + public @Nullable T getValue() { return CollectionUtils.lastElement(values); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateValidationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateValidationTests.java index 18da8c516d..fd1b70f3c7 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateValidationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateValidationTests.java @@ -25,6 +25,7 @@ import java.util.Set; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -39,7 +40,6 @@ import org.springframework.data.mongodb.core.schema.MongoJsonSchema; import org.springframework.data.mongodb.test.util.Client; import org.springframework.data.mongodb.test.util.MongoClientExtension; -import org.springframework.lang.Nullable; import org.springframework.test.context.junit.jupiter.SpringExtension; import com.mongodb.client.MongoClient; @@ -242,13 +242,11 @@ public SimpleBean(@Nullable String nonNullString, @Nullable Integer rangedIntege this.customFieldName = customFieldName; } - @Nullable - public String getNonNullString() { + public @Nullable String getNonNullString() { return this.nonNullString; } - @Nullable - public Integer getRangedInteger() { + public @Nullable Integer getRangedInteger() { return this.rangedInteger; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Person.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Person.java index bc126e05f0..7b07cd9448 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Person.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Person.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core; import org.bson.types.ObjectId; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; public class Person { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryResultConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryResultConverterUnitTests.java new file mode 100644 index 0000000000..107b942161 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryResultConverterUnitTests.java @@ -0,0 +1,92 @@ +/* + * 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.mongodb.core; + +import static org.assertj.core.api.Assertions.*; + +import org.bson.Document; +import org.junit.jupiter.api.Test; + +import org.springframework.data.mongodb.core.QueryResultConverter.ConversionResultSupplier; + +/** + * Unit tests for {@link QueryResultConverter}. + * + * @author Christoph Strobl + */ +class QueryResultConverterUnitTests { + + public static final ConversionResultSupplier ERROR_SUPPLIER = () -> { + throw new IllegalStateException("must not read conversion result"); + }; + + @Test // GH-4949 + void converterDoesNotEagerlyRetrieveConversionResultFromSupplier() { + + QueryResultConverter converter = new QueryResultConverter() { + + @Override + public String mapDocument(Document document, ConversionResultSupplier reader) { + return "done"; + } + }; + + assertThat(converter.mapDocument(new Document(), ERROR_SUPPLIER)).isEqualTo("done"); + } + + @Test // GH-4949 + void converterPassesOnConversionResultToNextStage() { + + Document source = new Document("value", "10"); + + QueryResultConverter stagedConverter = new QueryResultConverter() { + + @Override + public String mapDocument(Document document, ConversionResultSupplier reader) { + return document.get("value", "-1"); + } + }.andThen(new QueryResultConverter() { + + @Override + public Integer mapDocument(Document document, ConversionResultSupplier reader) { + + assertThat(document).isEqualTo(source); + return Integer.valueOf(reader.get()); + } + }); + + assertThat(stagedConverter.mapDocument(source, ERROR_SUPPLIER)).isEqualTo(10); + } + + @Test // GH-4949 + void entityConverterDelaysConversion() { + + Document source = new Document("value", "10"); + + QueryResultConverter converter = QueryResultConverter. entity() + .andThen(new QueryResultConverter() { + + @Override + public Integer mapDocument(Document document, ConversionResultSupplier reader) { + + assertThat(document).isEqualTo(source); + return Integer.valueOf(document.get("value", "20")); + } + }); + + assertThat(converter.mapDocument(source, ERROR_SUPPLIER)).isEqualTo(10); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupportUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupportUnitTests.java index 9d4ed339b5..83e1b3c272 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupportUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupportUnitTests.java @@ -72,7 +72,8 @@ void aggregateWithUntypedAggregationAndExplicitCollection() { opSupport.aggregateAndReturn(Person.class).inCollection("star-wars").by(newAggregation(project("foo"))).all(); ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); - verify(template).aggregate(any(Aggregation.class), eq("star-wars"), captor.capture()); + verify(template).doAggregate(any(Aggregation.class), eq("star-wars"), captor.capture(), any(Class.class), + eq(QueryResultConverter.entity())); assertThat(captor.getValue()).isEqualTo(Person.class); } @@ -86,7 +87,8 @@ void aggregateWithUntypedAggregation() { ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); verify(template).getCollectionName(captor.capture()); - verify(template).aggregate(any(Aggregation.class), eq("person"), captor.capture()); + verify(template).doAggregate(any(Aggregation.class), eq("person"), captor.capture(), any(Class.class), + eq(QueryResultConverter.entity())); assertThat(captor.getAllValues()).containsExactly(Person.class, Person.class); } @@ -101,7 +103,8 @@ void aggregateWithTypeAggregation() { ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); verify(template).getCollectionName(captor.capture()); - verify(template).aggregate(any(Aggregation.class), eq("person"), captor.capture()); + verify(template).doAggregate(any(Aggregation.class), eq("person"), captor.capture(), any(Class.class), + eq(QueryResultConverter.entity())); assertThat(captor.getAllValues()).containsExactly(Person.class, Jedi.class); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupportTests.java index f23e973202..d51696dd74 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupportTests.java @@ -26,6 +26,7 @@ import java.util.Date; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; @@ -167,6 +168,17 @@ void findAllWithProjection() { .verifyComplete(); } + @Test // GH-4949 + void findAllWithConverter() { + + template.query(Person.class).as(Jedi.class).map((document, reader) -> Optional.of(reader.get())).all() + .map(Optional::get) // + .map(it -> it.getClass().getName()) // + .as(StepVerifier::create) // + .expectNext(Jedi.class.getName(), Jedi.class.getName()) // + .verifyComplete(); + } + @Test // DATAMONGO-1719 void findAllBy() { @@ -299,6 +311,32 @@ void findAllNearByWithCollectionAndProjection() { .verifyComplete(); } + @Test // GH-4949 + @DirtiesState + void findAllNearByWithConverter() { + + blocking.indexOps(Planet.class).ensureIndex( + new GeospatialIndex("coordinates").typed(GeoSpatialIndexType.GEO_2DSPHERE).named("planet-coordinate-idx")); + + Planet alderan = new Planet("alderan", new Point(-73.9836, 40.7538)); + Planet dantooine = new Planet("dantooine", new Point(-73.9928, 40.7193)); + + blocking.save(alderan); + blocking.save(dantooine); + + template.query(Object.class).inCollection(STAR_WARS).as(Human.class) + .near(NearQuery.near(-73.9667, 40.78).spherical(true)).map((document, reader) -> Optional.of(reader.get())) // + .all() // + .as(StepVerifier::create).consumeNextWith(actual -> { + assertThat(actual.getDistance()).isNotNull(); + assertThat(actual.getContent()).isInstanceOf(Optional.class); + assertThat(actual.getContent().get()).isInstanceOf(Human.class); + assertThat(actual.getContent().get().getId()).isEqualTo("alderan"); + }) // + .expectNextCount(1) // + .verifyComplete(); + } + @Test // DATAMONGO-1719 @DirtiesState void findAllNearByReturningGeoResultContentAsClosedInterfaceProjection() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupportUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupportUnitTests.java index 609a456912..b5a40f5738 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupportUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupportUnitTests.java @@ -74,7 +74,7 @@ void usesExtractedCollectionName() { mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).all(); verify(template).mapReduce(any(Query.class), eq(Person.class), eq(STAR_WARS), eq(Person.class), eq(MAP_FUNCTION), - eq(REDUCE_FUNCTION), isNull()); + eq(REDUCE_FUNCTION), any()); } @Test // DATAMONGO-1929 @@ -84,7 +84,7 @@ void usesExplicitCollectionName() { .inCollection("the-night-angel").all(); verify(template).mapReduce(any(Query.class), eq(Person.class), eq("the-night-angel"), eq(Person.class), - eq(MAP_FUNCTION), eq(REDUCE_FUNCTION), isNull()); + eq(MAP_FUNCTION), eq(REDUCE_FUNCTION), any()); } @Test // DATAMONGO-1929 @@ -108,7 +108,7 @@ void usesQueryWhenPresent() { mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).matching(query).all(); verify(template).mapReduce(eq(query), eq(Person.class), eq(STAR_WARS), eq(Person.class), eq(MAP_FUNCTION), - eq(REDUCE_FUNCTION), isNull()); + eq(REDUCE_FUNCTION), any()); } @Test // DATAMONGO-2416 @@ -121,7 +121,7 @@ void usesCriteriaWhenPresent() { .matching(where("lastname").is("skywalker")).all(); verify(template).mapReduce(eq(query), eq(Person.class), eq(STAR_WARS), eq(Person.class), eq(MAP_FUNCTION), - eq(REDUCE_FUNCTION), isNull()); + eq(REDUCE_FUNCTION), any()); } @Test // DATAMONGO-1929 @@ -132,7 +132,7 @@ void usesProjectionWhenPresent() { mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).as(Jedi.class).all(); verify(template).mapReduce(any(Query.class), eq(Person.class), eq(STAR_WARS), eq(Jedi.class), eq(MAP_FUNCTION), - eq(REDUCE_FUNCTION), isNull()); + eq(REDUCE_FUNCTION), any()); } interface Contact {} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java index 80dd584b9e..f87227cdde 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java @@ -48,6 +48,7 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.GenericApplicationContext; import org.springframework.dao.DataIntegrityViolationException; @@ -84,6 +85,7 @@ import com.mongodb.WriteConcern; import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoCollection; /** * Integration test for {@link MongoTemplate}. @@ -165,6 +167,19 @@ void insertCollectionSetsId() { assertThat(person.getId()).isNotNull(); } + @Test // GH-4944 + void insertAllShouldConvertIdToTargetTypeBeforeSave() { + + RawStringId walter = new RawStringId(); + walter.value = "walter"; + + RawStringId returned = template.insertAll(List.of(walter)).blockLast(); + template.execute(RawStringId.class, MongoCollection::find) // + .as(StepVerifier::create) // + .consumeNextWith(actual -> assertThat(returned.id).isEqualTo(actual.get("_id"))) // + .verifyComplete(); + } + @Test // DATAMONGO-1444 void saveSetsId() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java index f89b2fa8c1..36cf0886ad 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; +import static org.springframework.data.mongodb.test.util.Assertions.*; import static org.springframework.data.mongodb.test.util.Assertions.assertThat; import reactor.core.publisher.Flux; @@ -43,6 +44,7 @@ import org.bson.Document; import org.bson.conversions.Bson; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -53,6 +55,7 @@ import org.mockito.quality.Strictness; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; + import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; @@ -93,7 +96,6 @@ import org.springframework.data.mongodb.core.timeseries.Granularity; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; -import org.springframework.lang.Nullable; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.CollectionUtils; @@ -437,7 +439,7 @@ void geoNearShouldHonorReadConcernFromQuery() { void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() { template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class, - PersonProjection.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + PersonProjection.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher).projection(eq(new Document("firstname", 1))); } @@ -446,7 +448,7 @@ void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() { void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() { template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document("bar", 1), Person.class, - PersonProjection.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + PersonProjection.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher).projection(eq(new Document("bar", 1))); } @@ -455,7 +457,7 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() { void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() { template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class, - PersonSpELProjection.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + PersonSpELProjection.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher, never()).projection(any()); } @@ -464,7 +466,7 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() { void appliesFieldsToDtoProjection() { template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class, - Jedi.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + Jedi.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher).projection(eq(new Document("firstname", 1))); } @@ -473,7 +475,7 @@ void appliesFieldsToDtoProjection() { void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() { template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document("bar", 1), Person.class, - Jedi.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + Jedi.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher).projection(eq(new Document("bar", 1))); } @@ -482,7 +484,7 @@ void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() { void doesNotApplyFieldsWhenTargetIsNotAProjection() { template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class, - Person.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + Person.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher, never()).projection(any()); } @@ -491,7 +493,7 @@ void doesNotApplyFieldsWhenTargetIsNotAProjection() { void doesNotApplyFieldsWhenTargetExtendsDomainType() { template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class, - PersonExtended.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + PersonExtended.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher, never()).projection(any()); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupportTests.java index 5659869705..cfdc5fe1a1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupportTests.java @@ -15,13 +15,14 @@ */ package org.springframework.data.mongodb.core; -import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.mongodb.core.query.Criteria.*; -import static org.springframework.data.mongodb.core.query.Query.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.core.query.Query.query; import reactor.test.StepVerifier; import java.util.Objects; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -108,6 +109,15 @@ void removeAndReturnAllMatching() { .expectNext(han).verifyComplete(); } + @Test // GH-4949 + void removeConvertAndReturnAllMatching() { + + template.remove(Person.class).matching(query(where("firstname").is("han"))).map((raw, it) -> Optional.of(it.get())) + .findAndRemove().as(StepVerifier::create).expectNext(Optional.of(han)).verifyComplete(); + + template.findById(han.id, Person.class).as(StepVerifier::create).verifyComplete(); + } + @org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS) static class Person { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java index 73970d2ad3..02637e9971 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java @@ -21,6 +21,7 @@ import java.lang.reflect.Proxy; +import com.mongodb.reactivestreams.client.ListCollectionNamesPublisher; import org.bson.Document; import org.bson.codecs.BsonValueCodec; import org.bson.codecs.configuration.CodecRegistry; @@ -58,7 +59,6 @@ import com.mongodb.reactivestreams.client.MongoClient; import com.mongodb.reactivestreams.client.MongoCollection; import com.mongodb.reactivestreams.client.MongoDatabase; -import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; /** * Unit tests for {@link ReactiveSessionBoundMongoTemplate}. @@ -94,7 +94,7 @@ public class ReactiveSessionBoundMongoTemplateUnitTests { @Before public void setUp() { - mock(MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(database).collectionNamePublisherType()); + mock(ListCollectionNamesPublisher.class); when(client.getDatabase(anyString())).thenReturn(database); when(codecRegistry.get(any(Class.class))).thenReturn(new BsonValueCodec()); when(database.getCodecRegistry()).thenReturn(codecRegistry); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java index 3ac99c2b6d..bef67501b3 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java @@ -15,13 +15,15 @@ */ package org.springframework.data.mongodb.core; -import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.mongodb.core.query.Criteria.*; -import static org.springframework.data.mongodb.core.query.Query.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.core.query.Query.query; import reactor.test.StepVerifier; import java.util.Objects; +import java.util.Optional; import org.bson.BsonString; import org.junit.jupiter.api.BeforeEach; @@ -175,6 +177,18 @@ void findAndModifyWithDifferentDomainTypeAndCollection() { "Han"); } + @Test // GH-4949 + void findAndModifyWithWithResultConversion() { + + template.update(Jedi.class).inCollection(STAR_WARS).matching(query(where("_id").is(han.getId()))) + .apply(new Update().set("name", "Han")).map((raw, it) -> Optional.of(it.get())).findAndModify() + .as(StepVerifier::create).consumeNextWith(actual -> assertThat(actual.get().getName()).isEqualTo("han")) + .verifyComplete(); + + assertThat(blocking.findOne(queryHan(), Person.class)).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", + "Han"); + } + @Test // DATAMONGO-1719 void findAndModifyWithOptions() { @@ -225,6 +239,18 @@ void findAndReplaceWithProjection() { }).verifyComplete(); } + @Test // GH-4949 + void findAndReplaceWithResultConversion() { + + Person luke = new Person(); + luke.firstname = "Luke"; + + template.update(Person.class).matching(queryHan()).replaceWith(luke).map((raw, it) -> Optional.of(it.get())).findAndReplace() // + .as(StepVerifier::create).consumeNextWith(it -> { + assertThat(it.get().getFirstname()).isEqualTo(han.firstname); + }).verifyComplete(); + } + @Test // DATAMONGO-1827 void findAndReplaceWithCollection() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java index dfa4b00515..50ea579044 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java @@ -49,7 +49,6 @@ import com.mongodb.client.model.DeleteOptions; import com.mongodb.client.model.FindOneAndUpdateOptions; import com.mongodb.client.model.UpdateOptions; -import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; /** * Unit test for {@link SessionBoundMongoTemplate} making sure a proxied {@link MongoCollection} and @@ -90,7 +89,7 @@ public class SessionBoundMongoTemplateUnitTests { @Before public void setUp() { - collectionNamesIterable = mock(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(database).collectionNameIterableType()); + collectionNamesIterable = mock(ListCollectionNamesIterable.class); when(client.getDatabase(anyString())).thenReturn(database); when(codecRegistry.get(any(Class.class))).thenReturn(new BsonValueCodec()); when(database.getCodecRegistry()).thenReturn(codecRegistry); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/TransactionOptionsTestService.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/TransactionOptionsTestService.java index 8968f53a74..b8cc9cc972 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/TransactionOptionsTestService.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/TransactionOptionsTestService.java @@ -18,7 +18,7 @@ import java.util.function.Function; import java.util.function.UnaryOperator; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.transaction.annotation.Transactional; /** @@ -48,45 +48,38 @@ public T saveWithinMaxCommitTime(T entity) { return saveFunction.apply(entity); } - @Nullable @Transactional(transactionManager = "txManager", label = { "mongo:readConcern=available" }) - public T availableReadConcernFind(Object id) { + public @Nullable T availableReadConcernFind(Object id) { return findByIdFunction.apply(id); } - @Nullable @Transactional(transactionManager = "txManager", label = { "mongo:readConcern=invalid" }) - public T invalidReadConcernFind(Object id) { + public @Nullable T invalidReadConcernFind(Object id) { return findByIdFunction.apply(id); } - @Nullable @Transactional(transactionManager = "txManager", label = { "mongo:readConcern=${tx.read.concern}" }) - public T environmentReadConcernFind(Object id) { + public @Nullable T environmentReadConcernFind(Object id) { return findByIdFunction.apply(id); } - @Nullable @Transactional(transactionManager = "txManager", label = { "mongo:readConcern=majority" }) - public T majorityReadConcernFind(Object id) { + public @Nullable T majorityReadConcernFind(Object id) { return findByIdFunction.apply(id); } - @Nullable @Transactional(transactionManager = "txManager", label = { "mongo:readPreference=primaryPreferred" }) - public T findFromPrimaryPreferredReplica(Object id) { + public @Nullable T findFromPrimaryPreferredReplica(Object id) { return findByIdFunction.apply(id); } - @Nullable @Transactional(transactionManager = "txManager", label = { "mongo:readPreference=invalid" }) - public T findFromInvalidReplica(Object id) { + public @Nullable T findFromInvalidReplica(Object id) { return findByIdFunction.apply(id); } - @Nullable @Transactional(transactionManager = "txManager", label = { "mongo:readPreference=primary" }) - public T findFromPrimaryReplica(Object id) { + public @Nullable T findFromPrimaryReplica(Object id) { return findByIdFunction.apply(id); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java index d4c2f37f63..61cd3ecce4 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java @@ -20,6 +20,8 @@ import java.util.Arrays; import org.bson.Document; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.mongodb.CodecRegistryProvider; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; @@ -29,8 +31,6 @@ import org.springframework.data.mongodb.core.convert.UpdateMapper; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import com.mongodb.MongoClientSettings; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/User.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/User.java index 25fbbbcb83..e9eae082e3 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/User.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/User.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; public class User { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Venue.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Venue.java index 09a0605ed7..13aba70afa 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Venue.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Venue.java @@ -19,7 +19,7 @@ import java.util.Date; import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.mongodb.core.mapping.Document; @Document("newyork") @@ -30,7 +30,7 @@ public class Venue { private double[] location; private Date openingDate; - @PersistenceConstructor + @PersistenceCreator Venue(String name, double[] location) { super(); this.name = name; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java index 32c6d43220..3256b889d4 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java @@ -20,13 +20,13 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.lang.Nullable; /** * Unit tests for {@link AddFieldsOperation}. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java index 99579b34a7..1495dec1c6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java @@ -37,6 +37,7 @@ import java.util.Date; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Scanner; import java.util.stream.Stream; @@ -287,6 +288,60 @@ void shouldAggregateEmptyCollectionAndStream() { } } + @Test // GH-4949 + void shouldAggregateAsStreamWithConverter() { + + MongoCollection coll = mongoTemplate.getCollection(INPUT_COLLECTION); + + coll.insertOne(createDocument("Doc1", "spring", "mongodb", "nosql")); + coll.insertOne(createDocument("Doc2")); + + Aggregation aggregation = newAggregation(// + project("tags"), // + unwind("tags"), // + group("tags") // + .count().as("n"), // + project("n") // + .and("tag").previousOperation(), // + sort(DESC, "n") // + ); + + try (Stream> stream = mongoTemplate.aggregateAndReturn(TagCount.class) + .inCollection(INPUT_COLLECTION).by(aggregation).map((document, reader) -> Optional.of(reader.get())).stream()) { + + List tagCount = stream.flatMap(Optional::stream).toList(); + + assertThat(tagCount).hasSize(3); + } + } + + @Test // GH-4949 + void shouldAggregateWithConverter() { + + MongoCollection coll = mongoTemplate.getCollection(INPUT_COLLECTION); + + coll.insertOne(createDocument("Doc1", "spring", "mongodb", "nosql")); + coll.insertOne(createDocument("Doc2")); + + Aggregation aggregation = newAggregation(// + project("tags"), // + unwind("tags"), // + group("tags") // + .count().as("n"), // + project("n") // + .and("tag").previousOperation(), // + sort(DESC, "n") // + ); + + AggregationResults> results = mongoTemplate.aggregateAndReturn(TagCount.class) + .inCollection(INPUT_COLLECTION) // + .by(aggregation) // + .map((document, reader) -> Optional.of(reader.get())) // + .all(); + + assertThat(results.getMappedResults()).extracting(Optional::get).hasOnlyElementsOfType(TagCount.class).hasSize(3); + } + @Test // DATAMONGO-1391 void shouldUnwindWithIndex() { @@ -501,7 +556,7 @@ void findStatesWithPopulationOver10MillionAggregationExample() { /* //complex mongodb aggregation framework example from https://docs.mongodb.org/manual/tutorial/aggregation-examples/#largest-and-smallest-cities-by-state - + db.zipcodes.aggregate( { $group: { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java index 007fdbb28c..0ab5545f23 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.aggregation; -import static org.springframework.data.mongodb.test.util.Assertions.*; +import static org.springframework.data.mongodb.test.util.Assertions.assertThat; import java.util.ArrayList; import java.util.Arrays; @@ -24,6 +24,7 @@ import org.bson.Document; import org.junit.jupiter.api.Test; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.aggregation.ArrayOperators.ArrayToObject; /** @@ -179,4 +180,26 @@ void sortByWithFieldRef() { assertThat(ArrayOperators.arrayOf("team").sort(Sort.by("name")).toDocument(Aggregation.DEFAULT_CONTEXT)) .isEqualTo("{ $sortArray: { input: \"$team\", sortBy: { name: 1 } } }"); } + + @Test // GH-4929 + public void sortArrayByValueAscending() { + + Document result = ArrayOperators.arrayOf("numbers").sort(Direction.ASC).toDocument(Aggregation.DEFAULT_CONTEXT); + assertThat(result).isEqualTo("{ $sortArray: { input: '$numbers', sortBy: 1 } }"); + } + + @Test // GH-4929 + public void sortArrayByValueDescending() { + + Document result = ArrayOperators.arrayOf("numbers").sort(Direction.DESC).toDocument(Aggregation.DEFAULT_CONTEXT); + assertThat(result).isEqualTo("{ $sortArray: { input: '$numbers', sortBy: -1 } }"); + } + + @Test // GH-4929 + void sortByWithDirection() { + + assertThat(ArrayOperators.arrayOf(List.of("a", "b", "d", "c")).sort(Direction.DESC) + .toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo("{ $sortArray: { input: [\"a\", \"b\", \"d\", \"c\"], sortBy: -1 } }"); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DensifyOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DensifyOperationUnitTests.java index 47176fd8ab..d830a44582 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DensifyOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DensifyOperationUnitTests.java @@ -19,6 +19,7 @@ import java.util.Date; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.mongodb.core.aggregation.DensifyOperation.DensifyUnits; import org.springframework.data.mongodb.core.aggregation.DensifyOperation.Range; @@ -27,7 +28,6 @@ import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.lang.Nullable; /** * Unit tests for {@link DensifyOperation}. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java index 9496a51c03..5f66e61bdc 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java @@ -20,6 +20,7 @@ import java.util.Arrays; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.geo.Distance; @@ -33,7 +34,6 @@ import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.lang.Nullable; /** * Unit tests for {@link GeoNearOperation}. @@ -70,7 +70,7 @@ public void rendersNearQueryWithKeyCorrectly() { @Test // DATAMONGO-2264 public void rendersMaxDistanceCorrectly() { - NearQuery query = NearQuery.near(10.0, 20.0).maxDistance(new Distance(30.0)); + NearQuery query = NearQuery.near(10.0, 20.0).maxDistance(Distance.of(30.0)); assertThat(new GeoNearOperation(query, "distance").toPipelineStages(Aggregation.DEFAULT_CONTEXT)) .containsExactly($geoNear().near(10.0, 20.0).maxDistance(30.0).doc()); @@ -79,7 +79,7 @@ public void rendersMaxDistanceCorrectly() { @Test // DATAMONGO-2264 public void rendersMinDistanceCorrectly() { - NearQuery query = NearQuery.near(10.0, 20.0).minDistance(new Distance(30.0)); + NearQuery query = NearQuery.near(10.0, 20.0).minDistance(Distance.of(30.0)); assertThat(new GeoNearOperation(query, "distance").toPipelineStages(Aggregation.DEFAULT_CONTEXT)) .containsExactly($geoNear().near(10.0, 20.0).minDistance(30.0).doc()); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/MergeOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/MergeOperationUnitTests.java index 311496ba8d..18980f6a06 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/MergeOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/MergeOperationUnitTests.java @@ -23,6 +23,7 @@ import java.util.Arrays; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Sum; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; @@ -30,7 +31,6 @@ import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.lang.Nullable; /** * Unit tests for {@link MergeOperation}. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/Order.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/Order.java index 1174507e1c..a463b72cff 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/Order.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/Order.java @@ -20,6 +20,8 @@ import java.util.Date; import java.util.List; +import org.springframework.lang.Contract; + /** * @author Thomas Darimont */ diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationTests.java index 55d6bf3b60..92c1d2cd97 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationTests.java @@ -22,6 +22,7 @@ import reactor.test.StepVerifier; import java.util.Arrays; +import java.util.Optional; import org.bson.Document; import org.junit.After; @@ -115,6 +116,29 @@ public void shouldProjectMultipleDocuments() { }).verifyComplete(); } + @Test // GH-4949 + public void shouldProjectAndConvertMultipleDocuments() { + + City dresden = new City("Dresden", 100); + City linz = new City("Linz", 101); + City braunschweig = new City("Braunschweig", 102); + City weinheim = new City("Weinheim", 103); + + reactiveMongoTemplate.insertAll(Arrays.asList(dresden, linz, braunschweig, weinheim)).as(StepVerifier::create) + .expectNextCount(4).verifyComplete(); + + Aggregation agg = newAggregation( // + match(where("population").lt(103))); + + reactiveMongoTemplate.aggregateAndReturn(City.class).inCollection("city").by(agg) + .map((document, reader) -> Optional.of(reader.get())) // + .all() // + .collectList() // + .as(StepVerifier::create).consumeNextWith(actual -> { + assertThat(actual).hasSize(3).extracting(Optional::get).contains(dresden, linz, braunschweig); + }).verifyComplete(); + } + @Test // DATAMONGO-1646 public void shouldAggregateToOutCollection() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/RedactOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/RedactOperationUnitTests.java index 24566089e7..d29e32a988 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/RedactOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/RedactOperationUnitTests.java @@ -20,6 +20,7 @@ import java.util.Arrays; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; @@ -27,7 +28,6 @@ import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.lang.Nullable; /** * Unit tests for {@link RedactOperation}. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java index 093d4af7a0..82ad4c223f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java @@ -20,6 +20,7 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; @@ -27,7 +28,6 @@ import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.lang.Nullable; /** * Unit tests for {@link SetOperation}. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperationUnitTests.java index b5f5f596e6..18eb659cd0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperationUnitTests.java @@ -20,6 +20,7 @@ import java.util.Date; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; @@ -30,7 +31,6 @@ import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.lang.Nullable; /** * Unit tests for {@link SetWindowFieldsOperation}. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperationUnitTests.java index e47fea289e..ef514ca882 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperationUnitTests.java @@ -21,13 +21,13 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.lang.Nullable; /** * Unit tests for {@link UnionWithOperation}. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnsetOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnsetOperationUnitTests.java index 2f081cc9fc..c406b89626 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnsetOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnsetOperationUnitTests.java @@ -22,6 +22,7 @@ import java.util.Collections; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; @@ -29,7 +30,6 @@ import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.lang.Nullable; /** * Unit tests for {@link UnsetOperation}. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperationUnitTests.java index 4ce045fe6f..936460f466 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperationUnitTests.java @@ -15,14 +15,14 @@ */ package org.springframework.data.mongodb.core.aggregation; -import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.mongodb.test.util.Assertions.assertThat; import java.util.List; import org.bson.Document; import org.junit.jupiter.api.Test; - import org.springframework.data.annotation.Id; +import org.springframework.data.domain.Limit; import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation.SearchType; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.query.Criteria; @@ -103,6 +103,16 @@ void mapsCriteriaToDomainType() { .containsExactly(new Document("$vectorSearch", new Document($VECTOR_SEARCH).append("filter", filter))); } + @Test + void withInvalidLimit() { + + VectorSearchOperation $search = VectorSearchOperation.search("vector_index").path("plot_embedding") + .vector(-0.0016261312, -0.028070757, -0.011342932).limit(Limit.unlimited()); + + List stages = $search.toPipelineStages(TestAggregationContext.contextFor(Movie.class)); + assertThat(stages.get(0)).doesNotContainKey("$vectorSearch.limit"); + } + static class Movie { @Id String id; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java index b53531f301..71c395e822 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java @@ -34,6 +34,7 @@ import org.bson.Document; import org.bson.conversions.Bson; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledForJreRange; @@ -46,7 +47,7 @@ import org.springframework.data.annotation.AccessType; import org.springframework.data.annotation.AccessType.Type; import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mongodb.MongoDatabaseFactory; @@ -55,7 +56,6 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.lang.Nullable; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.SerializationUtils; @@ -774,7 +774,7 @@ static class LazyDbRefTargetWithPeristenceConstructor extends LazyDbRefTarget { public LazyDbRefTargetWithPeristenceConstructor() {} - @PersistenceConstructor + @PersistenceCreator LazyDbRefTargetWithPeristenceConstructor(String id, String value) { super(id, value); this.persistenceConstructorCalled = true; @@ -790,7 +790,7 @@ static class LazyDbRefTargetWithPeristenceConstructorWithoutDefaultConstructor e boolean persistenceConstructorCalled; - @PersistenceConstructor + @PersistenceCreator LazyDbRefTargetWithPeristenceConstructorWithoutDefaultConstructor(String id, String value) { super(id, value); this.persistenceConstructorCalled = true; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/GeoConvertersUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/GeoConvertersUnitTests.java index 7fb664b00c..84a494f9d8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/GeoConvertersUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/GeoConvertersUnitTests.java @@ -69,7 +69,7 @@ public void convertsCircleToDocumentAndBackCorrectlyNeutralDistance() { @Test // DATAMONGO-858 public void convertsCircleToDocumentAndBackCorrectlyMilesDistance() { - Distance radius = new Distance(3, Metrics.MILES); + Distance radius = Distance.of(3, Metrics.MILES); Circle circle = new Circle(new Point(1, 2), radius); Document document = CircleToDocumentConverter.INSTANCE.convert(circle); @@ -106,7 +106,7 @@ public void convertsSphereToDocumentAndBackCorrectlyWithNeutralDistance() { @Test // DATAMONGO-858 public void convertsSphereToDocumentAndBackCorrectlyWithKilometerDistance() { - Distance radius = new Distance(3, Metrics.KILOMETERS); + Distance radius = Distance.of(3, Metrics.KILOMETERS); Sphere sphere = new Sphere(new Point(1, 2), radius); Document document = SphereToDocumentConverter.INSTANCE.convert(sphere); @@ -160,7 +160,7 @@ public void convertsCircleCorrectlyWhenUsingNonDoubleForCoordinates() { circle.put("radius", 3L); assertThat(DocumentToCircleConverter.INSTANCE.convert(circle)) - .isEqualTo(new Circle(new Point(1, 2), new Distance(3))); + .isEqualTo(new Circle(new Point(1, 2), Distance.of(3))); } @Test // DATAMONGO-1607 @@ -171,7 +171,7 @@ public void convertsSphereCorrectlyWhenUsingNonDoubleForCoordinates() { sphere.put("radius", 3L); assertThat(DocumentToSphereConverter.INSTANCE.convert(sphere)) - .isEqualTo(new Sphere(new Point(1, 2), new Distance(3))); + .isEqualTo(new Sphere(new Point(1, 2), Distance.of(3))); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index cf6d69c6c3..6f1c7439c0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -40,6 +40,8 @@ import org.bson.types.Code; import org.bson.types.Decimal128; import org.bson.types.ObjectId; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -63,7 +65,7 @@ import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.annotation.Transient; import org.springframework.data.annotation.TypeAlias; import org.springframework.data.convert.ConverterBuilder; @@ -103,10 +105,7 @@ import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback; import org.springframework.data.projection.EntityProjection; import org.springframework.data.projection.EntityProjectionIntrospector; -import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.test.util.ReflectionTestUtils; import com.mongodb.BasicDBList; @@ -1061,7 +1060,7 @@ void convertsSetToBasicDBList() { address.city = "London"; address.street = "Foo"; - Object result = converter.convertToMongoType(Collections.singleton(address), ClassTypeInformation.OBJECT); + Object result = converter.convertToMongoType(Collections.singleton(address), TypeInformation.OBJECT); assertThat(result).isInstanceOf(List.class); Set readResult = converter.read(Set.class, (org.bson.Document) result); @@ -1393,7 +1392,7 @@ void convertsListToBasicDBListAndRetainsTypeInformationForComplexObjects() { address.street = "Foo"; Object result = converter.convertToMongoType(Collections.singletonList(address), - ClassTypeInformation.from(InterfaceType.class)); + TypeInformation.of(InterfaceType.class)); assertThat(result).isInstanceOf(List.class); @@ -1421,7 +1420,7 @@ void convertsArrayToBasicDBListAndRetainsTypeInformationForComplexObjects() { address.city = "London"; address.street = "Foo"; - Object result = converter.convertToMongoType(new Address[] { address }, ClassTypeInformation.OBJECT); + Object result = converter.convertToMongoType(new Address[] { address }, TypeInformation.OBJECT); assertThat(result).isInstanceOf(List.class); @@ -1627,7 +1626,7 @@ void shouldWriteEntityWithGeoSphereCorrectly() { void shouldWriteEntityWithGeoSphereWithMetricDistanceCorrectly() { ClassWithGeoSphere object = new ClassWithGeoSphere(); - Sphere sphere = new Sphere(new Point(1, 2), new Distance(3, Metrics.KILOMETERS)); + Sphere sphere = new Sphere(new Point(1, 2), Distance.of(3, Metrics.KILOMETERS)); Distance radius = sphere.getRadius(); object.sphere = sphere; @@ -1712,7 +1711,7 @@ void shouldIncludeTextScorePropertyWhenReading() { } @Test // DATAMONGO-1001, DATAMONGO-1509 - void shouldWriteCglibProxiedClassTypeInformationCorrectly() { + void shouldWriteCglibProxiedTypeInformationCorrectly() { ProxyFactory factory = new ProxyFactory(); factory.setTargetClass(GenericType.class); @@ -2318,7 +2317,7 @@ void readAndConvertDBRefNestedByMapCorrectly() { Mockito.doReturn(cluster).when(spyConverter).readRef(dbRef); Map result = spyConverter.readMap(spyConverter.getConversionContext(ObjectPath.ROOT), data, - ClassTypeInformation.MAP); + TypeInformation.MAP); assertThat(((Map) result.get("cluster")).get("_id")).isEqualTo(100L); } @@ -3168,15 +3167,14 @@ void beanConverter() { registrar.registerConverter(WithValueConverters.class, "viaRegisteredConverter", new PropertyValueConverter() { - @Nullable @Override - public String read(@Nullable org.bson.Document nativeValue, MongoConversionContext context) { + public @Nullable String read(org.bson.@Nullable Document nativeValue, MongoConversionContext context) { return nativeValue.getString("bar"); } - @Nullable + @Override - public org.bson.Document write(@Nullable String domainValue, MongoConversionContext context) { + public org.bson.@Nullable Document write(@Nullable String domainValue, MongoConversionContext context) { return new org.bson.Document("bar", domainValue); } }); @@ -3522,7 +3520,7 @@ static class Person implements Contact { } - @PersistenceConstructor + @PersistenceCreator public Person(Set
addresses) { this.addresses = addresses; } @@ -3802,7 +3800,7 @@ static class PrimitiveContainer { @Field("property") private int m_property; - @PersistenceConstructor + @PersistenceCreator public PrimitiveContainer(@Value("#root.property") int a_property) { m_property = a_property; } @@ -3817,7 +3815,7 @@ static class ObjectContainer { @Field("property") private PrimitiveContainer m_property; - @PersistenceConstructor + @PersistenceCreator public ObjectContainer(@Value("#root.property") PrimitiveContainer a_property) { m_property = a_property; } @@ -4084,8 +4082,7 @@ static class WithExplicitTargetTypes { @Field(targetType = FieldType.DECIMAL128) // BigDecimal bigDecimal; - @Field(targetType = FieldType.DECIMAL128) - BigInteger bigInteger; + @Field(targetType = FieldType.DECIMAL128) BigInteger bigInteger; @Field(targetType = FieldType.INT64) // Date dateAsLong; @@ -4215,9 +4212,9 @@ public SubTypeOfGenericType convert(org.bson.Document source) { @WritingConverter static class TypeImplementingMapToDocumentConverter implements Converter { - @Nullable + @Override - public org.bson.Document convert(TypeImplementingMap source) { + public org.bson.@Nullable Document convert(TypeImplementingMap source) { return new org.bson.Document("1st", source.val1).append("2nd", source.val2); } } @@ -4225,9 +4222,8 @@ public org.bson.Document convert(TypeImplementingMap source) { @ReadingConverter static class DocumentToTypeImplementingMapConverter implements Converter { - @Nullable @Override - public TypeImplementingMap convert(org.bson.Document source) { + public @Nullable TypeImplementingMap convert(org.bson.Document source) { return new TypeImplementingMap(source.getString("1st"), source.getInteger("2nd")); } } @@ -4413,30 +4409,28 @@ enum Converter2 implements MongoValueConverter { INSTANCE; - @Nullable @Override - public String read(@Nullable org.bson.Document value, MongoConversionContext context) { + public @Nullable String read(org.bson.@Nullable Document value, MongoConversionContext context) { return value.getString("bar"); } - @Nullable + @Override - public org.bson.Document write(@Nullable String value, MongoConversionContext context) { + public org.bson.@Nullable Document write(@Nullable String value, MongoConversionContext context) { return new org.bson.Document("bar", value); } } static class Converter1 implements MongoValueConverter { - @Nullable @Override - public String read(@Nullable org.bson.Document value, MongoConversionContext context) { + public @Nullable String read(org.bson.@Nullable Document value, MongoConversionContext context) { return value.getString("foo"); } - @Nullable + @Override - public org.bson.Document write(@Nullable String value, MongoConversionContext context) { + public org.bson.@Nullable Document write(@Nullable String value, MongoConversionContext context) { return new org.bson.Document("foo", value); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ObjectPathUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ObjectPathUnitTests.java index b772772444..9e58693faa 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ObjectPathUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ObjectPathUnitTests.java @@ -23,7 +23,7 @@ import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; -import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; /** * Unit tests for {@link ObjectPath}. @@ -39,9 +39,9 @@ public class ObjectPathUnitTests { @BeforeEach public void setUp() { - one = new BasicMongoPersistentEntity<>(ClassTypeInformation.from(EntityOne.class)); - two = new BasicMongoPersistentEntity<>(ClassTypeInformation.from(EntityTwo.class)); - three = new BasicMongoPersistentEntity<>(ClassTypeInformation.from(EntityThree.class)); + one = new BasicMongoPersistentEntity<>(TypeInformation.of(EntityOne.class)); + two = new BasicMongoPersistentEntity<>(TypeInformation.of(EntityTwo.class)); + three = new BasicMongoPersistentEntity<>(TypeInformation.of(EntityThree.class)); } @Test // DATAMONGO-1703 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ReversingValueConverter.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ReversingValueConverter.java index eb3b1aba1a..0fe791784d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ReversingValueConverter.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ReversingValueConverter.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.convert; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * @author Christoph Strobl diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java index d8e36c8f67..c646af5539 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java @@ -544,7 +544,7 @@ void doesNotConvertRawDocuments() { } @Test // DATAMONG0-471 - void testUpdateShouldRetainClassTypeInformationWhenUsing$addToSetWith$eachForCustomTypes() { + void testUpdateShouldRetainTypeInformationWhenUsing$addToSetWith$eachForCustomTypes() { Update update = new Update().addToSet("models").each(new ModelImpl(2014), new ModelImpl(1), new ModelImpl(28)); Document mappedObject = mapper.getMappedObject(update.getUpdateObject(), diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoQueryableEncryptionCollectionCreationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoQueryableEncryptionCollectionCreationTests.java new file mode 100644 index 0000000000..dd9e459e78 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoQueryableEncryptionCollectionCreationTests.java @@ -0,0 +1,142 @@ +/* + * 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.mongodb.core.encryption; + +import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.*; +import static org.springframework.data.mongodb.core.schema.QueryCharacteristics.*; +import static org.springframework.data.mongodb.test.util.Assertions.*; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; + +import org.bson.BsonBinary; +import org.bson.Document; +import org.bson.UuidRepresentation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +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.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.core.CollectionOptions; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.schema.JsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.MongoJsonSchema; +import org.springframework.data.mongodb.test.util.Client; +import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion; +import org.springframework.data.mongodb.test.util.MongoClientExtension; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import com.mongodb.client.MongoClient; + +/** + * Integration tests for creating collections with encrypted fields. + * + * @author Christoph Strobl + */ +@ExtendWith({ MongoClientExtension.class, SpringExtension.class }) +@EnableIfMongoServerVersion(isGreaterThanEqual = "8.0") +@ContextConfiguration +public class MongoQueryableEncryptionCollectionCreationTests { + + public static final String COLLECTION_NAME = "enc-collection"; + static @Client MongoClient mongoClient; + + @Configuration + static class Config extends AbstractMongoClientConfiguration { + + @Override + public MongoClient mongoClient() { + return mongoClient; + } + + @Override + protected String getDatabaseName() { + return "encryption-schema-tests"; + } + + } + + @Autowired MongoTemplate template; + + @BeforeEach + void beforeEach() { + template.dropCollection(COLLECTION_NAME); + } + + @ParameterizedTest // GH-4185 + @MethodSource("collectionOptions") + public void createsCollectionWithEncryptedFieldsCorrectly(CollectionOptions collectionOptions) { + + template.createCollection(COLLECTION_NAME, collectionOptions); + + Document encryptedFields = readEncryptedFieldsFromDatabase(COLLECTION_NAME); + assertThat(encryptedFields).containsKey("fields"); + + List fields = encryptedFields.get("fields", List.of()); + assertThat(fields.get(0)).containsEntry("path", "encryptedInt") // + .containsEntry("bsonType", "int") // + .containsEntry("queries", List + .of(Document.parse("{'queryType': 'range', 'contention': { '$numberLong' : '1' }, 'min': 5, 'max': 100}"))); + + assertThat(fields.get(1)).containsEntry("path", "nested.encryptedLong") // + .containsEntry("bsonType", "long") // + .containsEntry("queries", List.of(Document.parse( + "{'queryType': 'range', 'contention': { '$numberLong' : '0' }, 'min': { '$numberLong' : '-1' }, 'max': { '$numberLong' : '1' }}"))); + } + + private static Stream collectionOptions() { + + BsonBinary key1 = new BsonBinary(UUID.randomUUID(), UuidRepresentation.STANDARD); + BsonBinary key2 = new BsonBinary(UUID.randomUUID(), UuidRepresentation.STANDARD); + + CollectionOptions manualOptions = CollectionOptions.encryptedCollection(options -> options // + .queryable(encrypted(int32("encryptedInt")).keys(key1), range().min(5).max(100).contention(1)) // + .queryable(encrypted(JsonSchemaProperty.int64("nested.encryptedLong")).keys(key2), + range().min(-1L).max(1L).contention(0))); + + CollectionOptions schemaOptions = CollectionOptions.encryptedCollection(MongoJsonSchema.builder() + .property( + queryable(encrypted(int32("encryptedInt")).keyId(key1), List.of(range().min(5).max(100).contention(1)))) + .property(queryable(encrypted(int64("nested.encryptedLong")).keyId(key2), + List.of(range().min(-1L).max(1L).contention(0)))) + .build()); + + return Stream.of(Arguments.of(manualOptions), Arguments.of(schemaOptions)); + } + + Document readEncryptedFieldsFromDatabase(String collectionName) { + + Document collectionInfo = template + .executeCommand(new Document("listCollections", 1).append("filter", new Document("name", collectionName))); + + if (collectionInfo.containsKey("cursor")) { + collectionInfo = (Document) collectionInfo.get("cursor", Document.class).get("firstBatch", List.class).iterator() + .next(); + } + + if (!collectionInfo.containsKey("options")) { + return new Document(); + } + + return collectionInfo.get("options", Document.class).get("encryptedFields", Document.class); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java new file mode 100644 index 0000000000..e4e760cc91 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java @@ -0,0 +1,573 @@ +/* + * 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.mongodb.core.encryption; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.mongodb.core.query.Criteria.*; + +import java.security.SecureRandom; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import org.assertj.core.api.Assumptions; +import org.bson.BsonBinary; +import org.bson.BsonDocument; +import org.bson.BsonInt32; +import org.bson.BsonString; +import org.bson.Document; +import org.junit.Before; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.data.convert.PropertyValueConverterFactory; +import org.springframework.data.convert.ValueConverter; +import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.core.CollectionOptions; +import org.springframework.data.mongodb.core.CollectionOptions.EncryptedFieldsOptions; +import org.springframework.data.mongodb.core.MongoJsonSchemaCreator; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; +import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter; +import org.springframework.data.mongodb.core.mapping.Encrypted; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.Queryable; +import org.springframework.data.mongodb.core.mapping.RangeEncrypted; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.core.schema.MongoJsonSchema; +import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion; +import org.springframework.data.mongodb.test.util.EnableIfReplicaSetAvailable; +import org.springframework.data.mongodb.test.util.MongoClientExtension; +import org.springframework.data.mongodb.util.MongoClientVersion; +import org.springframework.data.util.Lazy; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.util.StringUtils; + +import com.mongodb.AutoEncryptionSettings; +import com.mongodb.ClientEncryptionSettings; +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoNamespace; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.CreateEncryptedCollectionParams; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.client.model.Indexes; +import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.model.vault.RangeOptions; +import com.mongodb.client.result.UpdateResult; +import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.client.vault.ClientEncryptions; + +/** + * @author Ross Lawley + * @author Christoph Strobl + */ +@ExtendWith({ MongoClientExtension.class, SpringExtension.class }) +@EnableIfMongoServerVersion(isGreaterThanEqual = "8.0") +@EnableIfReplicaSetAvailable +@ContextConfiguration(classes = RangeEncryptionTests.EncryptionConfig.class) +class RangeEncryptionTests { + + @Autowired MongoTemplate template; + @Autowired MongoClientEncryption clientEncryption; + @Autowired EncryptionKeyHolder keyHolder; + + @BeforeEach + void clientVersionCheck() { + Assumptions.assumeThat(MongoClientVersion.isVersion5orNewer()).isTrue(); + } + + @AfterEach + void tearDown() { + template.getDb().getCollection("test").deleteMany(new BsonDocument()); + } + + @Test // GH-4185 + void manuallyEncryptedValuesCanBeSavedAndRetrievedCorrectly() { + + EncryptOptions encryptOptions = new EncryptOptions("Range").contentionFactor(1L) + .keyId(keyHolder.getEncryptionKey("encryptedInt")) + .rangeOptions(new RangeOptions().min(new BsonInt32(0)).max(new BsonInt32(200)).sparsity(1L)); + + EncryptOptions encryptExpressionOptions = new EncryptOptions("Range").contentionFactor(1L) + .rangeOptions(new RangeOptions().min(new BsonInt32(0)).max(new BsonInt32(200))) + .keyId(keyHolder.getEncryptionKey("encryptedInt")).queryType("range"); + + EncryptOptions equalityEncOptions = new EncryptOptions("Indexed").contentionFactor(0L) + .keyId(keyHolder.getEncryptionKey("age")); + ; + + EncryptOptions equalityEncOptionsString = new EncryptOptions("Indexed").contentionFactor(0L) + .keyId(keyHolder.getEncryptionKey("name")); + ; + + Document source = new Document("_id", "id-1"); + + source.put("name", + clientEncryption.getClientEncryption().encrypt(new BsonString("It's a Me, Mario!"), equalityEncOptionsString)); + source.put("age", clientEncryption.getClientEncryption().encrypt(new BsonInt32(101), equalityEncOptions)); + source.put("encryptedInt", clientEncryption.getClientEncryption().encrypt(new BsonInt32(101), encryptOptions)); + source.put("_class", Person.class.getName()); + + template.execute(Person.class, col -> col.insertOne(source)); + + Document result = template.execute(Person.class, col -> { + + BsonDocument filterSource = new BsonDocument("encryptedInt", new BsonDocument("$gte", new BsonInt32(100))); + BsonDocument filter = clientEncryption.getClientEncryption() + .encryptExpression(new Document("$and", List.of(filterSource)), encryptExpressionOptions); + + return col.find(filter).first(); + }); + + assertThat(result).containsEntry("encryptedInt", 101); + } + + @Test // GH-4185 + void canLesserThanEqualMatchRangeEncryptedField() { + + Person source = createPerson(); + template.insert(source); + + Person loaded = template.query(Person.class).matching(where("encryptedInt").lte(source.encryptedInt)).firstValue(); + assertThat(loaded).isEqualTo(source); + } + + @Test // GH-4185 + void canQueryMixOfEqualityEncryptedAndUnencrypted() { + + Person source = template.insert(createPerson()); + + Person loaded = template.query(Person.class) + .matching(where("name").is(source.name).and("unencryptedValue").is(source.unencryptedValue)).firstValue(); + assertThat(loaded).isEqualTo(source); + } + + @Test // GH-4185 + void canQueryMixOfRangeEncryptedAndUnencrypted() { + + Person source = template.insert(createPerson()); + + Person loaded = template.query(Person.class) + .matching(where("encryptedInt").lte(source.encryptedInt).and("unencryptedValue").is(source.unencryptedValue)) + .firstValue(); + assertThat(loaded).isEqualTo(source); + } + + @Test // GH-4185 + void canQueryEqualityEncryptedField() { + + Person source = createPerson(); + template.insert(source); + + Person loaded = template.query(Person.class).matching(where("age").is(source.age)).firstValue(); + assertThat(loaded).isEqualTo(source); + } + + @Test // GH-4185 + void canExcludeSafeContentFromResult() { + + Person source = createPerson(); + template.insert(source); + + Query q = Query.query(where("encryptedLong").lte(1001L).gte(1001L)); + q.fields().exclude("__safeContent__"); + + Person loaded = template.query(Person.class).matching(q).firstValue(); + assertThat(loaded).isEqualTo(source); + } + + @Test // GH-4185 + void canRangeMatchRangeEncryptedField() { + + Person source = createPerson(); + template.insert(source); + + Query q = Query.query(where("encryptedLong").lte(1001L).gte(1001L)); + Person loaded = template.query(Person.class).matching(q).firstValue(); + assertThat(loaded).isEqualTo(source); + } + + @Test // GH-4185 + void canReplaceEntityWithRangeEncryptedField() { + + Person source = createPerson(); + template.insert(source); + + source.encryptedInt = 123; + source.encryptedLong = 9999L; + template.save(source); + + Person loaded = template.query(Person.class).matching(where("id").is(source.id)).firstValue(); + assertThat(loaded).isEqualTo(source); + } + + @Test // GH-4185 + void canUpdateRangeEncryptedField() { + + Person source = createPerson(); + template.insert(source); + + UpdateResult updateResult = template.update(Person.class).matching(where("id").is(source.id)) + .apply(Update.update("encryptedLong", 5000L)).first(); + assertThat(updateResult.getModifiedCount()).isOne(); + + Person loaded = template.query(Person.class).matching(where("id").is(source.id)).firstValue(); + assertThat(loaded.encryptedLong).isEqualTo(5000L); + } + + @Test // GH-4185 + void errorsWhenUsingNonRangeOperatorEqOnRangeEncryptedField() { + + Person source = createPerson(); + template.insert(source); + + assertThatThrownBy( + () -> template.query(Person.class).matching(where("encryptedInt").is(source.encryptedInt)).firstValue()) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith("Not a valid range query. Querying a range encrypted field but " + + "the query operator '$eq' for field path 'encryptedInt' is not a range query."); + } + + @Test // GH-4185 + void errorsWhenUsingNonRangeOperatorInOnRangeEncryptedField() { + + Person source = createPerson(); + template.insert(source); + + assertThatThrownBy( + () -> template.query(Person.class).matching(where("encryptedLong").in(1001L, 9999L)).firstValue()) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith("Not a valid range query. Querying a range encrypted field but " + + "the query operator '$in' for field path 'encryptedLong' is not a range query."); + } + + private Person createPerson() { + + Person source = new Person(); + source.id = "id-1"; + source.unencryptedValue = "y2k"; + source.name = "it's a me mario!"; + source.age = 42; + source.encryptedInt = 101; + source.encryptedLong = 1001L; + source.nested = new NestedWithQEFields(); + source.nested.value = "Luigi time!"; + return source; + } + + protected static class EncryptionConfig extends AbstractMongoClientConfiguration { + + private static final String LOCAL_KMS_PROVIDER = "local"; + + private static final Lazy>> LAZY_KMS_PROVIDERS = Lazy.of(() -> { + byte[] localMasterKey = new byte[96]; + new SecureRandom().nextBytes(localMasterKey); + return Map.of(LOCAL_KMS_PROVIDER, Map.of("key", localMasterKey)); + }); + + @Autowired ApplicationContext applicationContext; + + @Override + protected String getDatabaseName() { + return "qe-test"; + } + + @Bean + public MongoClient mongoClient() { + return super.mongoClient(); + } + + @Override + protected void configureConverters(MongoConverterConfigurationAdapter converterConfigurationAdapter) { + converterConfigurationAdapter + .registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(applicationContext)) + .useNativeDriverJavaTimeCodecs(); + } + + @Bean + EncryptionKeyHolder keyHolder(MongoClientEncryption mongoClientEncryption) { + + Lazy> lazyDataKeyMap = Lazy.of(() -> { + try (MongoClient client = mongoClient()) { + + MongoDatabase database = client.getDatabase(getDatabaseName()); + database.getCollection("test").drop(); + + ClientEncryption clientEncryption = mongoClientEncryption.getClientEncryption(); + + MongoJsonSchema personSchema = MongoJsonSchemaCreator.create(new MongoMappingContext()) // init schema creator + .filter(MongoJsonSchemaCreator.encryptedOnly()) // + .createSchemaFor(Person.class); // + + Document encryptedFields = CollectionOptions.encryptedCollection(personSchema) // + .getEncryptedFieldsOptions() // + .map(EncryptedFieldsOptions::toDocument) // + .orElseThrow(); + + CreateCollectionOptions createCollectionOptions = new CreateCollectionOptions() + .encryptedFields(encryptedFields); + + BsonDocument local = clientEncryption.createEncryptedCollection(database, "test", createCollectionOptions, + new CreateEncryptedCollectionParams(LOCAL_KMS_PROVIDER)); + + Map keyMap = new LinkedHashMap<>(); + for (Object o : local.getArray("fields")) { + if (o instanceof BsonDocument db) { + String path = db.getString("path").getValue(); + BsonBinary binary = db.getBinary("keyId"); + for (String part : path.split("\\.")) { + keyMap.put(part, binary); + } + } + } + return keyMap; + } + }); + + return new EncryptionKeyHolder(lazyDataKeyMap); + } + + @Bean + MongoEncryptionConverter encryptingConverter(MongoClientEncryption mongoClientEncryption, + EncryptionKeyHolder keyHolder) { + return new MongoEncryptionConverter(mongoClientEncryption, EncryptionKeyResolver.annotated((ctx) -> { + + String path = ctx.getProperty().getFieldName(); + + if (ctx.getProperty().getMongoField().getName().isPath()) { + path = StringUtils.arrayToDelimitedString(ctx.getProperty().getMongoField().getName().parts(), "."); + } + if (ctx.getOperatorContext() != null) { + path = ctx.getOperatorContext().path(); + } + return EncryptionKey.keyId(keyHolder.getEncryptionKey(path)); + })); + } + + @Bean + CachingMongoClientEncryption clientEncryption(ClientEncryptionSettings encryptionSettings) { + return new CachingMongoClientEncryption(() -> ClientEncryptions.create(encryptionSettings)); + } + + @Override + protected void configureClientSettings(MongoClientSettings.Builder builder) { + try (MongoClient client = MongoClients.create()) { + ClientEncryptionSettings clientEncryptionSettings = encryptionSettings(client); + + builder.autoEncryptionSettings(AutoEncryptionSettings.builder() // + .kmsProviders(clientEncryptionSettings.getKmsProviders()) // + .keyVaultNamespace(clientEncryptionSettings.getKeyVaultNamespace()) // + .bypassQueryAnalysis(true).build()); + } + } + + @Bean + ClientEncryptionSettings encryptionSettings(MongoClient mongoClient) { + MongoNamespace keyVaultNamespace = new MongoNamespace("encryption.testKeyVault"); + MongoCollection keyVaultCollection = mongoClient.getDatabase(keyVaultNamespace.getDatabaseName()) + .getCollection(keyVaultNamespace.getCollectionName()); + keyVaultCollection.drop(); + // Ensure that two data keys cannot share the same keyAltName. + keyVaultCollection.createIndex(Indexes.ascending("keyAltNames"), + new IndexOptions().unique(true).partialFilterExpression(Filters.exists("keyAltNames"))); + + mongoClient.getDatabase(getDatabaseName()).getCollection("test").drop(); // Clear old data + + // Create the ClientEncryption instance + return ClientEncryptionSettings.builder() // + .keyVaultMongoClientSettings( + MongoClientSettings.builder().applyConnectionString(new ConnectionString("mongodb://localhost")).build()) // + .keyVaultNamespace(keyVaultNamespace.getFullName()) // + .kmsProviders(LAZY_KMS_PROVIDERS.get()) // + .build(); + } + } + + static class CachingMongoClientEncryption extends MongoClientEncryption implements DisposableBean { + + static final AtomicReference cache = new AtomicReference<>(); + + CachingMongoClientEncryption(Supplier source) { + super(() -> { + ClientEncryption clientEncryption = cache.get(); + if (clientEncryption == null) { + clientEncryption = source.get(); + cache.set(clientEncryption); + } + + return clientEncryption; + }); + } + + @Override + public void destroy() { + ClientEncryption clientEncryption = cache.get(); + if (clientEncryption != null) { + clientEncryption.close(); + cache.set(null); + } + } + } + + static class EncryptionKeyHolder { + + Supplier> lazyDataKeyMap; + + public EncryptionKeyHolder(Supplier> lazyDataKeyMap) { + this.lazyDataKeyMap = Lazy.of(lazyDataKeyMap); + } + + BsonBinary getEncryptionKey(String path) { + return lazyDataKeyMap.get().get(path); + } + } + + @org.springframework.data.mongodb.core.mapping.Document("test") + static class Person { + + String id; + + String unencryptedValue; + + @ValueConverter(MongoEncryptionConverter.class) + @Encrypted(algorithm = "Indexed") // + @Queryable(queryType = "equality", contentionFactor = 0) // + String name; + + @ValueConverter(MongoEncryptionConverter.class) + @Encrypted(algorithm = "Indexed") // + @Queryable(queryType = "equality", contentionFactor = 0) // + Integer age; + + @ValueConverter(MongoEncryptionConverter.class) + @RangeEncrypted(contentionFactor = 0L, + rangeOptions = "{\"min\": 0, \"max\": 200, \"trimFactor\": 1, \"sparsity\": 1}") // + Integer encryptedInt; + + @ValueConverter(MongoEncryptionConverter.class) + @RangeEncrypted(contentionFactor = 0L, + rangeOptions = "{\"min\": {\"$numberLong\": \"1000\"}, \"max\": {\"$numberLong\": \"9999\"}, \"trimFactor\": 1, \"sparsity\": 1}") // + Long encryptedLong; + + NestedWithQEFields nested; + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getEncryptedInt() { + return this.encryptedInt; + } + + public void setEncryptedInt(Integer encryptedInt) { + this.encryptedInt = encryptedInt; + } + + public Long getEncryptedLong() { + return this.encryptedLong; + } + + public void setEncryptedLong(Long encryptedLong) { + this.encryptedLong = encryptedLong; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Person person = (Person) o; + return Objects.equals(id, person.id) && Objects.equals(unencryptedValue, person.unencryptedValue) + && Objects.equals(name, person.name) && Objects.equals(age, person.age) + && Objects.equals(encryptedInt, person.encryptedInt) && Objects.equals(encryptedLong, person.encryptedLong); + } + + @Override + public int hashCode() { + return Objects.hash(id, unencryptedValue, name, age, encryptedInt, encryptedLong); + } + + @Override + public String toString() { + return "Person{" + "id='" + id + '\'' + ", unencryptedValue='" + unencryptedValue + '\'' + ", name='" + name + + '\'' + ", age=" + age + ", encryptedInt=" + encryptedInt + ", encryptedLong=" + encryptedLong + '}'; + } + } + + static class NestedWithQEFields { + + @ValueConverter(MongoEncryptionConverter.class) + @Encrypted(algorithm = "Indexed") // + @Queryable(queryType = "equality", contentionFactor = 0) // + String value; + + @Override + public String toString() { + return "NestedWithQEFields{" + "value='" + value + '\'' + '}'; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedWithQEFields that = (NestedWithQEFields) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java index b81b51abd5..96c685275f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java @@ -33,7 +33,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.dao.DataAccessException; import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; @@ -536,7 +536,7 @@ static class Venue2DSphere { private String name; private @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE) double[] location; - @PersistenceConstructor + @PersistenceCreator public Venue2DSphere(String name, double[] location) { this.name = name; this.location = location; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatial2DSphereTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatial2DSphereTests.java index 3a9140d34c..1774c36493 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatial2DSphereTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatial2DSphereTests.java @@ -23,9 +23,9 @@ import java.util.List; import org.junit.Test; + import org.springframework.data.domain.Sort.Direction; import org.springframework.data.geo.GeoResults; -import org.springframework.data.geo.Metric; import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.Venue; @@ -67,7 +67,7 @@ public void geoNearWithMinDistance() { GeoResults result = template.geoNear(geoNear, Venue.class); assertThat(result.getContent().size()).isNotEqualTo(0); - assertThat(result.getAverageDistance().getMetric()).isEqualTo((Metric) Metrics.KILOMETERS); + assertThat(result.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS); } @Test // DATAMONGO-1110 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java index aa26445f2d..dda16f7849 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java @@ -31,7 +31,6 @@ import org.junit.runner.RunWith; import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; - import org.springframework.core.annotation.AliasFor; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.annotation.Id; @@ -53,7 +52,7 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.Unwrapped; -import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; /** * Tests for {@link MongoPersistentEntityIndexResolver}. @@ -506,7 +505,7 @@ public void resolvesComposedAnnotationIndexDefinitionOptionsCorrectly() { assertThat(indexDefinition.getIndexKeys()).containsEntry("location", "geoHaystack").containsEntry("What light?", 1); assertThat(indexDefinition.getIndexOptions()).containsEntry("name", "my_geo_index_name") - .containsEntry("bucketSize", 2.0); + .doesNotContainKey("bucketSize"); } @Test // DATAMONGO-2112 @@ -558,9 +557,6 @@ class GeoSpatialIndexedDocumentWithComposedAnnotation { @AliasFor(annotation = GeoSpatialIndexed.class, attribute = "additionalField") String theAdditionalFieldINeedToDefine() default "What light?"; - @AliasFor(annotation = GeoSpatialIndexed.class, attribute = "bucketSize") - double size() default 2; - @AliasFor(annotation = GeoSpatialIndexed.class, attribute = "type") GeoSpatialIndexType indexType() default GeoSpatialIndexType.GEO_HAYSTACK; } @@ -1186,7 +1182,7 @@ public void shouldNotDetectCycleWhenTypeIsUsedMoreThanOnce() { @SuppressWarnings({ "rawtypes", "unchecked" }) public void shouldCatchCyclicReferenceExceptionOnRoot() { - MongoPersistentEntity entity = new BasicMongoPersistentEntity<>(ClassTypeInformation.from(Object.class)); + MongoPersistentEntity entity = new BasicMongoPersistentEntity<>(TypeInformation.of(Object.class)); MongoPersistentProperty propertyMock = mock(MongoPersistentProperty.class); when(propertyMock.isEntity()).thenReturn(true); @@ -1195,7 +1191,7 @@ public void shouldCatchCyclicReferenceExceptionOnRoot() { new MongoPersistentEntityIndexResolver.CyclicPropertyReferenceException("foo", Object.class, "bar")); MongoPersistentEntity selfCyclingEntity = new BasicMongoPersistentEntity<>( - ClassTypeInformation.from(SelfCyclingViaCollectionType.class)); + TypeInformation.of(SelfCyclingViaCollectionType.class)); new MongoPersistentEntityIndexResolver(prepareMappingContext(SelfCyclingViaCollectionType.class)) .resolveIndexForEntity(selfCyclingEntity); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/VectorIndexIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/VectorIndexIntegrationTests.java index dcd447f81a..387f075cb5 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/VectorIndexIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/VectorIndexIntegrationTests.java @@ -22,6 +22,7 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -34,7 +35,6 @@ import org.springframework.data.mongodb.test.util.AtlasContainer; import org.springframework.data.mongodb.test.util.MongoTestTemplate; import org.springframework.data.mongodb.test.util.MongoTestUtils; -import org.springframework.lang.Nullable; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -200,8 +200,7 @@ void createsVectorIndexWithFilters() throws InterruptedException { }); } - @Nullable - private Document readRawIndexInfo(String name) { + private @Nullable Document readRawIndexInfo(String name) { AggregateIterable indexes = template.execute(Movie.class, collection -> { return collection.aggregate(List.of(new Document("$listSearchIndexes", new Document("name", name)))); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java index 116505143e..1037ba4f19 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java @@ -39,7 +39,7 @@ import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy; import org.springframework.data.mapping.model.SimpleTypeHolder; -import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; import org.springframework.util.ReflectionUtils; /** @@ -56,7 +56,7 @@ public class BasicMongoPersistentPropertyUnitTests { @BeforeEach void setup() { - entity = new BasicMongoPersistentEntity<>(ClassTypeInformation.from(Person.class)); + entity = new BasicMongoPersistentEntity<>(TypeInformation.of(Person.class)); } @Test @@ -90,7 +90,7 @@ void preventsNegativeOrder() { void usesPropertyAccessForThrowableCause() { BasicMongoPersistentEntity entity = new BasicMongoPersistentEntity<>( - ClassTypeInformation.from(Throwable.class)); + TypeInformation.of(Throwable.class)); MongoPersistentProperty property = getPropertyFor(entity, "cause"); assertThat(property.usePropertyAccess()).isTrue(); @@ -99,7 +99,7 @@ void usesPropertyAccessForThrowableCause() { @Test // DATAMONGO-607 void usesCustomFieldNamingStrategyByDefault() throws Exception { - ClassTypeInformation type = ClassTypeInformation.from(Person.class); + TypeInformation type = TypeInformation.of(Person.class); Field field = ReflectionUtils.findField(Person.class, "lastname"); MongoPersistentProperty property = new BasicMongoPersistentProperty(Property.of(type, field), entity, @@ -116,7 +116,7 @@ void usesCustomFieldNamingStrategyByDefault() throws Exception { @Test // DATAMONGO-607 void rejectsInvalidValueReturnedByFieldNamingStrategy() { - ClassTypeInformation type = ClassTypeInformation.from(Person.class); + TypeInformation type = TypeInformation.of(Person.class); Field field = ReflectionUtils.findField(Person.class, "lastname"); MongoPersistentProperty property = new BasicMongoPersistentProperty(Property.of(type, field), entity, @@ -255,7 +255,7 @@ private MongoPersistentProperty getPropertyFor(Field field) { } private static MongoPersistentProperty getPropertyFor(Class type, String fieldname) { - return getPropertyFor(new BasicMongoPersistentEntity<>(ClassTypeInformation.from(type)), fieldname); + return getPropertyFor(new BasicMongoPersistentEntity<>(TypeInformation.of(type)), fieldname); } private static MongoPersistentProperty getPropertyFor(MongoPersistentEntity entity, String fieldname) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/Person.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/Person.java index 06f0db6c35..eaed01fc3b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/Person.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/Person.java @@ -18,7 +18,7 @@ import java.util.List; import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.annotation.Transient; import org.springframework.data.mongodb.core.index.CompoundIndex; import org.springframework.data.mongodb.core.index.CompoundIndexes; @@ -44,7 +44,7 @@ public Person(Integer ssn) { this.ssn = ssn; } - @PersistenceConstructor + @PersistenceCreator public Person(Integer ssn, String firstName, String lastName, Integer age, T address) { this.ssn = ssn; this.firstName = firstName; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/PersonCustomIdName.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/PersonCustomIdName.java index a68fe0d531..4a4f7fb126 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/PersonCustomIdName.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/PersonCustomIdName.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core.mapping; import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; /** * @author Jon Brisbin @@ -30,7 +30,7 @@ public PersonCustomIdName(Integer ssn, String firstName) { this.firstName = firstName; } - @PersistenceConstructor + @PersistenceCreator public PersonCustomIdName(Integer ssn, String firstName, String lastName) { this.ssn = ssn; this.firstName = firstName; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java index 9bc1dc78aa..1d44bff5ad 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java @@ -408,7 +408,8 @@ public void publishesEventsForQuerydslFindQueries() { template.save(new Person("Boba", "Fett", 40)); MongoRepositoryFactory factory = new MongoRepositoryFactory(template); - MongoEntityInformation entityInformation = factory.getEntityInformation(Person.class); + MongoEntityInformation entityInformation = factory + .getEntityInformation(Person.class); QuerydslMongoPredicateExecutor executor = new QuerydslMongoPredicateExecutor<>(entityInformation, template); executor.findOne(QPerson.person.lastname.startsWith("Fe")); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/IndexUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/IndexUnitTests.java index 156b5b23c6..2ec31e49bc 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/IndexUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/IndexUnitTests.java @@ -80,9 +80,9 @@ public void testGeospatialIndex2DSphere() { public void testGeospatialIndexGeoHaystack() { GeospatialIndex i = new GeospatialIndex("location").typed(GeoSpatialIndexType.GEO_HAYSTACK) - .withAdditionalField("name").withBucketSize(40); + .withAdditionalField("name"); assertThat(i.getIndexKeys()).isEqualTo(Document.parse("{ \"location\" : \"geoHaystack\" , \"name\" : 1}")); - assertThat(i.getIndexOptions()).isEqualTo(Document.parse("{ \"bucketSize\" : 40.0}")); + assertThat(i.getIndexOptions()).isEqualTo(Document.parse("{ }")); } @Test diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java index bbdad047f2..fdfa840d58 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java @@ -17,6 +17,7 @@ package org.springframework.data.mongodb.core.query; import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.data.Offset.*; import static org.assertj.core.data.Offset.offset; import org.junit.jupiter.api.Test; @@ -34,7 +35,7 @@ public class MetricConversionUnitTests { @Test // DATAMONGO-1348 public void shouldConvertMilesToMeters() { - Distance distance = new Distance(1, Metrics.MILES); + Distance distance = Distance.of(1, Metrics.MILES); double distanceInMeters = MetricConversion.getDistanceInMeters(distance); assertThat(distanceInMeters).isCloseTo(1609.3438343d, offset(0.000000001)); @@ -43,7 +44,7 @@ public void shouldConvertMilesToMeters() { @Test // DATAMONGO-1348 public void shouldConvertKilometersToMeters() { - Distance distance = new Distance(1, Metrics.KILOMETERS); + Distance distance = Distance.of(1, Metrics.KILOMETERS); double distanceInMeters = MetricConversion.getDistanceInMeters(distance); assertThat(distanceInMeters).isCloseTo(1000, offset(0.000000001)); @@ -72,11 +73,13 @@ public void shouldCalculateMetersToMilesMultiplier() { @Test // GH-4004 void shouldConvertKilometersToRadians/* on an earth like sphere with r=6378.137km */() { - assertThat(MetricConversion.toRadians(new Distance(1, Metrics.KILOMETERS))).isCloseTo(0.000156785594d, offset(0.000000001)); + assertThat(MetricConversion.toRadians(Distance.of(1, Metrics.KILOMETERS))).isCloseTo(0.000156785594d, + offset(0.000000001)); } @Test // GH-4004 void shouldConvertMilesToRadians/* on an earth like sphere with r=6378.137km */() { - assertThat(MetricConversion.toRadians(new Distance(1, Metrics.MILES))).isCloseTo(0.000252321328d, offset(0.000000001)); + assertThat(MetricConversion.toRadians(Distance.of(1, Metrics.MILES))).isCloseTo(0.000252321328d, + offset(0.000000001)); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java index f4e3d26eb1..2b600988db 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java @@ -21,10 +21,10 @@ import java.math.RoundingMode; import org.junit.jupiter.api.Test; + import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.geo.Distance; -import org.springframework.data.geo.Metric; import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.DocumentTestUtils; @@ -44,7 +44,7 @@ */ public class NearQueryUnitTests { - private static final Distance ONE_FIFTY_KILOMETERS = new Distance(150, Metrics.KILOMETERS); + private static final Distance ONE_FIFTY_KILOMETERS = Distance.of(150, Metrics.KILOMETERS); @Test public void rejectsNullPoint() { @@ -57,7 +57,7 @@ public void settingUpNearWithMetricRecalculatesDistance() { NearQuery query = NearQuery.near(2.5, 2.5, Metrics.KILOMETERS).maxDistance(150); assertThat(query.getMaxDistance()).isEqualTo(ONE_FIFTY_KILOMETERS); - assertThat(query.getMetric()).isEqualTo((Metric) Metrics.KILOMETERS); + assertThat(query.getMetric()).isEqualTo(Metrics.KILOMETERS); assertThat(query.isSpherical()).isTrue(); } @@ -68,27 +68,27 @@ public void settingMetricRecalculatesMaxDistance() { query.inMiles(); - assertThat(query.getMetric()).isEqualTo((Metric) Metrics.MILES); + assertThat(query.getMetric()).isEqualTo(Metrics.MILES); } @Test public void configuresResultMetricCorrectly() { NearQuery query = NearQuery.near(2.5, 2.1); - assertThat(query.getMetric()).isEqualTo((Metric) Metrics.NEUTRAL); + assertThat(query.getMetric()).isEqualTo(Metrics.NEUTRAL); query = query.maxDistance(ONE_FIFTY_KILOMETERS); - assertThat(query.getMetric()).isEqualTo((Metric) Metrics.KILOMETERS); + assertThat(query.getMetric()).isEqualTo(Metrics.KILOMETERS); assertThat(query.getMaxDistance()).isEqualTo(ONE_FIFTY_KILOMETERS); assertThat(query.isSpherical()).isTrue(); query = query.in(Metrics.MILES); - assertThat(query.getMetric()).isEqualTo((Metric) Metrics.MILES); + assertThat(query.getMetric()).isEqualTo(Metrics.MILES); assertThat(query.getMaxDistance()).isEqualTo(ONE_FIFTY_KILOMETERS); assertThat(query.isSpherical()).isTrue(); - query = query.maxDistance(new Distance(200, Metrics.KILOMETERS)); - assertThat(query.getMetric()).isEqualTo((Metric) Metrics.MILES); + query = query.maxDistance(Distance.of(200, Metrics.KILOMETERS)); + assertThat(query.getMetric()).isEqualTo(Metrics.MILES); } @Test // DATAMONGO-445, DATAMONGO-2264 @@ -200,7 +200,7 @@ public void shouldUseMetersForGeoJsonData() { public void shouldUseMetersForGeoJsonDataWhenDistanceInKilometers() { NearQuery query = NearQuery.near(new GeoJsonPoint(27.987901, 86.9165379)); - query.maxDistance(new Distance(1, Metrics.KILOMETERS)); + query.maxDistance(Distance.of(1, Metrics.KILOMETERS)); assertThat(query.toDocument()).containsEntry("maxDistance", 1000D).containsEntry("distanceMultiplier", 0.001D); } @@ -209,7 +209,7 @@ public void shouldUseMetersForGeoJsonDataWhenDistanceInKilometers() { public void shouldUseMetersForGeoJsonDataWhenDistanceInMiles() { NearQuery query = NearQuery.near(new GeoJsonPoint(27.987901, 86.9165379)); - query.maxDistance(new Distance(1, Metrics.MILES)); + query.maxDistance(Distance.of(1, Metrics.MILES)); assertThat(query.toDocument()).containsEntry("maxDistance", 1609.3438343D).containsEntry("distanceMultiplier", 0.00062137D); @@ -219,7 +219,7 @@ public void shouldUseMetersForGeoJsonDataWhenDistanceInMiles() { public void shouldUseKilometersForDistanceWhenMaxDistanceInMiles() { NearQuery query = NearQuery.near(new GeoJsonPoint(27.987901, 86.9165379)); - query.maxDistance(new Distance(1, Metrics.MILES)).in(Metrics.KILOMETERS); + query.maxDistance(Distance.of(1, Metrics.MILES)).in(Metrics.KILOMETERS); assertThat(query.toDocument()).containsEntry("maxDistance", 1609.3438343D).containsEntry("distanceMultiplier", 0.001D); @@ -229,7 +229,7 @@ public void shouldUseKilometersForDistanceWhenMaxDistanceInMiles() { public void shouldUseMilesForDistanceWhenMaxDistanceInKilometers() { NearQuery query = NearQuery.near(new GeoJsonPoint(27.987901, 86.9165379)); - query.maxDistance(new Distance(1, Metrics.KILOMETERS)).in(Metrics.MILES); + query.maxDistance(Distance.of(1, Metrics.KILOMETERS)).in(Metrics.MILES); assertThat(query.toDocument()).containsEntry("maxDistance", 1000D).containsEntry("distanceMultiplier", 0.00062137D); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/TextQueryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/TextQueryTests.java index 6ea0f5aa9c..b12b83fe3a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/TextQueryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/TextQueryTests.java @@ -21,6 +21,7 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -35,7 +36,6 @@ import org.springframework.data.mongodb.test.util.MongoTemplateExtension; import org.springframework.data.mongodb.test.util.MongoTestTemplate; import org.springframework.data.mongodb.test.util.Template; -import org.springframework.lang.Nullable; /** * @author Christoph Strobl diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java index 3514927b18..1691305617 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java @@ -15,11 +15,14 @@ */ package org.springframework.data.mongodb.core.schema; +import static org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty.*; import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.*; +import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.encrypted; import static org.springframework.data.mongodb.test.util.Assertions.*; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.UUID; import org.bson.Document; @@ -105,6 +108,37 @@ void rendersEncryptedPropertyWithKeyIdCorrectly() { .append("algorithm", "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic").append("bsonType", "string")))))); } + @Test // GH-4185 + void rendersQueryablePropertyCorrectly() { + + MongoJsonSchema schema = MongoJsonSchema.builder().properties( // + queryable(rangeEncrypted(number("ssn")), + List.of(QueryCharacteristics.range().contention(0).trimFactor(1).sparsity(1).min(0).max(200)))) + .build(); + + assertThat(schema.toDocument().get("$jsonSchema", Document.class)).isEqualTo(""" + { + "type": "object", + "properties": { + "ssn": { + "encrypt": { + "bsonType": "long", + "algorithm": "Range", + "queries": [{ + "queryType": "range", + "contention": {$numberLong: "0"}, + "trimFactor": 1, + "sparsity": {$numberLong: "1"}, + "min": 0, + "max": 200 + }] + } + } + } + } + """); + } + @Test // DATAMONGO-1835 void throwsExceptionOnNullRoot() { assertThatIllegalArgumentException().isThrownBy(() -> MongoJsonSchema.of((JsonSchemaObject) null)); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/monitor/MongoMonitorIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/monitor/MongoMonitorIntegrationTests.java deleted file mode 100644 index e70b398f7f..0000000000 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/monitor/MongoMonitorIntegrationTests.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2002-2025 the original author 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.mongodb.monitor; - -import static org.assertj.core.api.Assertions.*; - -import java.net.UnknownHostException; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.data.mongodb.test.util.Client; -import org.springframework.data.mongodb.test.util.MongoClientExtension; - -import com.mongodb.client.MongoClient; - -/** - * This test class assumes that you are already running the MongoDB server. - * - * @author Mark Pollack - * @author Thomas Darimont - * @author Mark Paluch - */ -@ExtendWith(MongoClientExtension.class) -public class MongoMonitorIntegrationTests { - - static @Client MongoClient mongoClient; - - @Test - public void serverInfo() { - ServerInfo serverInfo = new ServerInfo(mongoClient); - serverInfo.getVersion(); - } - - @Test // DATAMONGO-685 - public void getHostNameShouldReturnServerNameReportedByMongo() throws UnknownHostException { - - ServerInfo serverInfo = new ServerInfo(mongoClient); - - String hostName = null; - try { - hostName = serverInfo.getHostName(); - } catch (UnknownHostException e) { - throw e; - } - - assertThat(hostName).isNotNull(); - assertThat(hostName).isEqualTo("127.0.0.1:27017"); - } - - @Test - public void operationCounters() { - OperationCounters operationCounters = new OperationCounters(mongoClient); - operationCounters.getInsertCount(); - } -} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/PerformanceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/PerformanceTests.java index e815cc6e7c..fb8cedd9b1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/PerformanceTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/PerformanceTests.java @@ -28,7 +28,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.core.Constants; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory; import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; @@ -454,7 +454,7 @@ public Address(String zipCode, String city) { this(zipCode, city, new HashSet(pickRandomNumerOfItemsFrom(Arrays.asList(AddressType.values())))); } - @PersistenceConstructor + @PersistenceCreator public Address(String zipCode, String city, Set types) { this.zipCode = zipCode; this.city = city; @@ -512,7 +512,7 @@ public Order(List lineItems, Date createdAt) { this.status = Status.ORDERED; } - @PersistenceConstructor + @PersistenceCreator public Order(List lineItems, Date createdAt, Status status) { this.lineItems = lineItems; this.createdAt = createdAt; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java index edda1aad01..a7fba9a046 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java @@ -28,11 +28,12 @@ import org.bson.Document; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.core.Constants; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.mongodb.core.ReactiveMongoOperations; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory; @@ -48,7 +49,6 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.data.mongodb.repository.support.ReactiveMongoRepositoryFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StopWatch; import org.springframework.util.StringUtils; @@ -99,9 +99,8 @@ public void setUp() throws Exception { converter = new MappingMongoConverter(new DbRefResolver() { - @Nullable @Override - public Object resolveReference(MongoPersistentProperty property, Object source, + public @Nullable Object resolveReference(MongoPersistentProperty property, Object source, ReferenceLookupDelegate referenceLookupDelegate, MongoEntityReader entityReader) { return null; } @@ -513,7 +512,7 @@ public Address(String zipCode, String city) { this(zipCode, city, new HashSet(pickRandomNumerOfItemsFrom(Arrays.asList(AddressType.values())))); } - @PersistenceConstructor + @PersistenceCreator public Address(String zipCode, String city, Set types) { this.zipCode = zipCode; this.city = city; @@ -571,7 +570,7 @@ public Order(List lineItems, Date createdAt) { this.status = Status.ORDERED; } - @PersistenceConstructor + @PersistenceCreator public Order(List lineItems, Date createdAt, Status status) { this.lineItems = lineItems; this.createdAt = createdAt; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index 3f2e60f4c4..c2cb6cacf8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -38,6 +38,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.IncorrectResultSizeDataAccessException; @@ -49,7 +50,6 @@ import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoPage; import org.springframework.data.geo.GeoResults; -import org.springframework.data.geo.Metric; import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; import org.springframework.data.geo.Polygon; @@ -458,7 +458,7 @@ void executesGeoNearQueryForResultsCorrectly() { repository.save(dave); GeoResults results = repository.findByLocationNear(new Point(-73.99, 40.73), - new Distance(2000, Metrics.KILOMETERS)); + Distance.of(2000, Metrics.KILOMETERS)); assertThat(results.getContent()).isNotEmpty(); } @@ -470,11 +470,11 @@ void executesGeoPageQueryForResultsCorrectly() { repository.save(dave); GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), - new Distance(2000, Metrics.KILOMETERS), PageRequest.of(0, 20)); + Distance.of(2000, Metrics.KILOMETERS), PageRequest.of(0, 20)); assertThat(results.getContent()).isNotEmpty(); // DATAMONGO-607 - assertThat(results.getAverageDistance().getMetric()).isEqualTo((Metric) Metrics.KILOMETERS); + assertThat(results.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS); } @Test // DATAMONGO-323 @@ -634,13 +634,13 @@ void executesGeoPageQueryForWithPageRequestForPageInBetween() { repository.saveAll(Arrays.asList(dave, oliver, carter, boyd, leroi)); GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), - new Distance(2000, Metrics.KILOMETERS), PageRequest.of(1, 2)); + Distance.of(2000, Metrics.KILOMETERS), PageRequest.of(1, 2)); assertThat(results.getContent()).isNotEmpty(); assertThat(results.getNumberOfElements()).isEqualTo(2); assertThat(results.isFirst()).isFalse(); assertThat(results.isLast()).isFalse(); - assertThat(results.getAverageDistance().getMetric()).isEqualTo((Metric) Metrics.KILOMETERS); + assertThat(results.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS); assertThat(results.getAverageDistance().getNormalizedValue()).isEqualTo(0.0); } @@ -656,12 +656,12 @@ void executesGeoPageQueryForWithPageRequestForPageAtTheEnd() { repository.saveAll(Arrays.asList(dave, oliver, carter)); GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), - new Distance(2000, Metrics.KILOMETERS), PageRequest.of(1, 2)); + Distance.of(2000, Metrics.KILOMETERS), PageRequest.of(1, 2)); assertThat(results.getContent()).isNotEmpty(); assertThat(results.getNumberOfElements()).isEqualTo(1); assertThat(results.isFirst()).isFalse(); assertThat(results.isLast()).isTrue(); - assertThat(results.getAverageDistance().getMetric()).isEqualTo((Metric) Metrics.KILOMETERS); + assertThat(results.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS); } @Test // DATAMONGO-445 @@ -672,13 +672,13 @@ void executesGeoPageQueryForWithPageRequestForJustOneElement() { repository.save(dave); GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), - new Distance(2000, Metrics.KILOMETERS), PageRequest.of(0, 2)); + Distance.of(2000, Metrics.KILOMETERS), PageRequest.of(0, 2)); assertThat(results.getContent()).isNotEmpty(); assertThat(results.getNumberOfElements()).isEqualTo(1); assertThat(results.isFirst()).isTrue(); assertThat(results.isLast()).isTrue(); - assertThat(results.getAverageDistance().getMetric()).isEqualTo((Metric) Metrics.KILOMETERS); + assertThat(results.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS); } @Test // DATAMONGO-445 @@ -688,13 +688,13 @@ void executesGeoPageQueryForWithPageRequestForJustOneElementEmptyPage() { repository.save(dave); GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), - new Distance(2000, Metrics.KILOMETERS), PageRequest.of(1, 2)); + Distance.of(2000, Metrics.KILOMETERS), PageRequest.of(1, 2)); assertThat(results.getContent()).isEmpty(); assertThat(results.getNumberOfElements()).isEqualTo(0); assertThat(results.isFirst()).isFalse(); assertThat(results.isLast()).isTrue(); - assertThat(results.getAverageDistance().getMetric()).isEqualTo((Metric) Metrics.KILOMETERS); + assertThat(results.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS); } @Test // DATAMONGO-1608 @@ -1117,7 +1117,7 @@ void executesGeoNearQueryForResultsCorrectlyWhenGivenMinAndMaxDistance() { dave.setLocation(point); repository.save(dave); - Range range = Distance.between(new Distance(0.01, KILOMETERS), new Distance(2000, KILOMETERS)); + Range range = Distance.between(Distance.of(0.01, KILOMETERS), Distance.of(2000, KILOMETERS)); GeoResults results = repository.findPersonByLocationNear(new Point(-73.99, 40.73), range); assertThat(results.getContent()).isNotEmpty(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Address.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Address.java index 534f44c8fb..be5be2d9ba 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Address.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Address.java @@ -14,8 +14,7 @@ * limitations under the License. */ package org.springframework.data.mongodb.repository; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.ObjectUtils; import com.querydsl.core.annotations.QueryEmbeddable; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoRepositoryTextSearchIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoRepositoryTextSearchIntegrationTests.java index c41abf4aa1..e0c2caee31 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoRepositoryTextSearchIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoRepositoryTextSearchIntegrationTests.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -38,7 +39,6 @@ import org.springframework.data.mongodb.test.util.MongoTemplateExtension; import org.springframework.data.mongodb.test.util.MongoTestTemplate; import org.springframework.data.mongodb.test.util.Template; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MyId.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MyId.java index 3dace8928b..4e589d5892 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MyId.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MyId.java @@ -17,7 +17,7 @@ import java.io.Serializable; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.ObjectUtils; /** diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java index 664b5279c8..eeca60bc3e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java @@ -21,6 +21,7 @@ import java.util.Set; import java.util.UUID; +import org.jspecify.annotations.Nullable; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.index.GeoSpatialIndexType; import org.springframework.data.mongodb.core.index.GeoSpatialIndexed; @@ -30,7 +31,6 @@ import org.springframework.data.mongodb.core.mapping.DocumentReference; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.Unwrapped; -import org.springframework.lang.Nullable; /** * Sample domain class. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java index 16b2157bc8..da22801ba6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java @@ -22,7 +22,7 @@ import java.util.Set; import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; /** * @author Christoph Strobl @@ -37,7 +37,7 @@ public PersonAggregate(String lastname, String name) { this(lastname, Collections.singletonList(name)); } - @PersistenceConstructor + @PersistenceCreator public PersonAggregate(String lastname, Collection names) { this.lastname = lastname; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index c66b554078..1f4f682ebc 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -23,6 +23,8 @@ import java.util.regex.Pattern; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.data.domain.Limit; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -43,7 +45,6 @@ import org.springframework.data.mongodb.repository.Person.Sex; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.query.Param; -import org.springframework.lang.Nullable; /** * Sample repository managing {@link Person} entities. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryTransactionalTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryTransactionalTests.java index 0af684b9c1..b2b350dc4d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryTransactionalTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryTransactionalTests.java @@ -27,6 +27,7 @@ import org.bson.Document; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -46,7 +47,6 @@ import org.springframework.data.mongodb.test.util.EnableIfReplicaSetAvailable; import org.springframework.data.mongodb.test.util.MongoClientExtension; import org.springframework.data.mongodb.test.util.ReplSetClient; -import org.springframework.lang.Nullable; import org.springframework.test.annotation.Rollback; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.transaction.AfterTransaction; @@ -205,9 +205,8 @@ private AfterTransactionAssertion assertAfterTransaction(Person person) { AfterTransactionAssertion assertion = new AfterTransactionAssertion<>(new Persistable() { - @Nullable @Override - public Object getId() { + public @Nullable Object getId() { return person.id; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java index e89dec21bd..2a76c0ba6c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java @@ -20,6 +20,7 @@ import static org.springframework.data.domain.Sort.Direction.*; import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.Query.*; +import static org.springframework.data.mongodb.test.util.Assertions.*; import static org.springframework.data.mongodb.test.util.Assertions.assertThat; import reactor.core.Disposable; @@ -40,6 +41,7 @@ import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; import org.reactivestreams.Publisher; + import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -72,7 +74,6 @@ import org.springframework.data.mongodb.test.util.ReactiveMongoClientClosingTestConfiguration; import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; import org.springframework.data.repository.Repository; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.test.context.junit.jupiter.SpringExtension; /** @@ -111,7 +112,6 @@ ReactiveMongoRepositoryFactory factory(ReactiveMongoOperations template, BeanFac factory.setRepositoryBaseClass(SimpleReactiveMongoRepository.class); factory.setBeanClassLoader(beanFactory.getClass().getClassLoader()); factory.setBeanFactory(beanFactory); - factory.setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT); return factory; } @@ -355,7 +355,7 @@ void findsPeopleGeoresultByLocationWithinBox() { repository.save(dave).as(StepVerifier::create).expectNextCount(1).verifyComplete(); repository.findByLocationNear(new Point(-73.99, 40.73), // - new Distance(2000, Metrics.KILOMETERS)).as(StepVerifier::create).consumeNextWith(actual -> { + Distance.of(2000, Metrics.KILOMETERS)).as(StepVerifier::create).consumeNextWith(actual -> { assertThat(actual.getDistance().getValue()).isCloseTo(1, offset(1d)); assertThat(actual.getContent()).isEqualTo(dave); @@ -374,7 +374,7 @@ void findsPeoplePageableGeoresultByLocationWithinBox() throws InterruptedExcepti Thread.sleep(500); repository.findByLocationNear(new Point(-73.99, 40.73), // - new Distance(2000, Metrics.KILOMETERS), // + Distance.of(2000, Metrics.KILOMETERS), // PageRequest.of(0, 10)).as(StepVerifier::create) // .consumeNextWith(actual -> { @@ -395,7 +395,7 @@ void findsPeopleByLocationWithinBox() throws InterruptedException { Thread.sleep(500); repository.findPersonByLocationNear(new Point(-73.99, 40.73), // - new Distance(2000, Metrics.KILOMETERS)).as(StepVerifier::create) // + Distance.of(2000, Metrics.KILOMETERS)).as(StepVerifier::create) // .expectNext(dave) // .verifyComplete(); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveVectorSearchTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveVectorSearchTests.java new file mode 100644 index 0000000000..14a4749c8a --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveVectorSearchTests.java @@ -0,0 +1,224 @@ +/* + * 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.mongodb.repository; + +import static org.assertj.core.api.Assertions.*; + +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import java.util.List; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.SearchResult; +import org.springframework.data.domain.Similarity; +import org.springframework.data.domain.Vector; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory; +import org.springframework.data.mongodb.core.TestMongoConfiguration; +import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.index.VectorIndex; +import org.springframework.data.mongodb.core.index.VectorIndex.SimilarityFunction; +import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories; +import org.springframework.data.mongodb.test.util.AtlasContainer; +import org.springframework.data.mongodb.test.util.MongoTestTemplate; +import org.springframework.data.repository.CrudRepository; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.mongodb.MongoDBAtlasLocalContainer; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; + +/** + * Integration tests using reactive Vector Search and Vector Indexes through local MongoDB Atlas. + * + * @author Mark Paluch + */ +@Testcontainers(disabledWithoutDocker = true) +@SpringJUnitConfig(classes = { ReactiveVectorSearchTests.Config.class }) +public class ReactiveVectorSearchTests { + + Vector VECTOR = Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f); + + private static final MongoDBAtlasLocalContainer atlasLocal = AtlasContainer.bestMatch().withReuse(true); + private static final String COLLECTION_NAME = "collection-1"; + + static MongoClient client; + static MongoTestTemplate template; + + @Autowired ReactiveVectorSearchRepository repository; + + @EnableReactiveMongoRepositories( + includeFilters = { + @ComponentScan.Filter(value = ReactiveVectorSearchRepository.class, type = FilterType.ASSIGNABLE_TYPE) }, + considerNestedRepositories = true) + static class Config extends TestMongoConfiguration { + + @Override + public String getDatabaseName() { + return "vector-search-tests"; + } + + @Override + public MongoClient mongoClient() { + atlasLocal.start(); + return MongoClients.create(atlasLocal.getConnectionString()); + } + + @Bean + public com.mongodb.reactivestreams.client.MongoClient reactiveMongoClient() { + atlasLocal.start(); + return com.mongodb.reactivestreams.client.MongoClients.create(atlasLocal.getConnectionString()); + } + + @Bean + ReactiveMongoTemplate reactiveMongoTemplate(MappingMongoConverter mongoConverter) { + return new ReactiveMongoTemplate(new SimpleReactiveMongoDatabaseFactory(reactiveMongoClient(), getDatabaseName()), + mongoConverter); + } + } + + @BeforeAll + static void beforeAll() throws InterruptedException { + atlasLocal.start(); + + System.out.println(atlasLocal.getConnectionString()); + client = MongoClients.create(atlasLocal.getConnectionString()); + template = new MongoTestTemplate(client, "vector-search-tests"); + + template.remove(WithVectorFields.class).all(); + initDocuments(); + initIndexes(); + + Thread.sleep(500); // just wait a little or the index will be broken + } + + @Test + void shouldSearchEnnWithAnnotatedFilter() { + + Flux> results = repository.searchAnnotated("de", VECTOR, Score.of(0.4), + Limit.of(10)); + + results.as(StepVerifier::create).consumeNextWith(actual -> { + assertThat(actual.getScore().getValue()).isGreaterThan(0.4); + assertThat(actual.getScore()).isInstanceOf(Similarity.class); + + }).expectNextCount(2).verifyComplete(); + } + + @Test + void shouldSearchEnnWithDerivedFilter() { + + Flux results = repository.searchByCountryAndEmbeddingNear("de", VECTOR, Limit.of(10)); + + results.as(StepVerifier::create).consumeNextWith(actual -> assertThat(actual).isInstanceOf(WithVectorFields.class)) + .expectNextCount(2).verifyComplete(); + } + + static void initDocuments() { + + WithVectorFields w1 = new WithVectorFields("de", "one", Vector.of(0.1001f, 0.22345f, 0.33456f, 0.44567f, 0.55678f)); + WithVectorFields w2 = new WithVectorFields("de", "two", Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f)); + WithVectorFields w3 = new WithVectorFields("en", "three", + Vector.of(0.9001f, 0.82345f, 0.73456f, 0.64567f, 0.55678f)); + WithVectorFields w4 = new WithVectorFields("de", "four", + Vector.of(0.9001f, 0.92345f, 0.93456f, 0.94567f, 0.95678f)); + + template.insertAll(List.of(w1, w2, w3, w4)); + } + + static void initIndexes() { + + VectorIndex cosIndex = new VectorIndex("cos-index") + .addVector("embedding", it -> it.similarity(SimilarityFunction.COSINE).dimensions(5)).addFilter("country"); + + template.searchIndexOps(WithVectorFields.class).createIndex(cosIndex); + + VectorIndex euclideanIndex = new VectorIndex("euc-index") + .addVector("embedding", it -> it.similarity(SimilarityFunction.EUCLIDEAN).dimensions(5)).addFilter("country"); + + VectorIndex inner = new VectorIndex("ip-index") + .addVector("embedding", it -> it.similarity(SimilarityFunction.DOT_PRODUCT).dimensions(5)).addFilter("country"); + + template.searchIndexOps(WithVectorFields.class).createIndex(cosIndex); + template.searchIndexOps(WithVectorFields.class).createIndex(euclideanIndex); + template.searchIndexOps(WithVectorFields.class).createIndex(inner); + template.awaitIndexCreation(WithVectorFields.class, cosIndex.getName()); + template.awaitIndexCreation(WithVectorFields.class, euclideanIndex.getName()); + template.awaitIndexCreation(WithVectorFields.class, inner.getName()); + } + + interface ReactiveVectorSearchRepository extends CrudRepository { + + @VectorSearch(indexName = "cos-index", filter = "{country: ?0}", numCandidates = "#{10+10}", + searchType = VectorSearchOperation.SearchType.ANN) + Flux> searchAnnotated(String country, Vector vector, Score distance, Limit limit); + + @VectorSearch(indexName = "cos-index") + Flux searchByCountryAndEmbeddingNear(String country, Vector vector, Limit limit); + + } + + @org.springframework.data.mongodb.core.mapping.Document(COLLECTION_NAME) + static class WithVectorFields { + + String id; + String country; + String description; + + Vector embedding; + + public WithVectorFields(String country, String description, Vector embedding) { + this.country = country; + this.description = description; + this.embedding = embedding; + } + + public String getId() { + return id; + } + + public String getCountry() { + return country; + } + + public String getDescription() { + return description; + } + + public Vector getEmbedding() { + return embedding; + } + + @Override + public String toString() { + return "WithVectorFields{" + "id='" + id + '\'' + ", country='" + country + '\'' + ", description='" + description + + '\'' + '}'; + } + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java index 44235c54ef..751fc51ded 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java @@ -26,6 +26,7 @@ import java.util.Objects; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; @@ -48,8 +49,6 @@ import org.springframework.data.mongodb.repository.support.SimpleReactiveMongoRepository; import org.springframework.data.mongodb.test.util.EnableIfReplicaSetAvailable; import org.springframework.data.repository.query.FluentQuery; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; -import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.TransactionDefinition; @@ -96,7 +95,6 @@ void setUp() { factory.setRepositoryBaseClass(SimpleReactiveMongoRepository.class); factory.setBeanClassLoader(classLoader); factory.setBeanFactory(beanFactory); - factory.setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT); repository = factory.getRepository(ReactivePersonRepository.class); immutableRepository = factory.getRepository(ReactiveImmutablePersonRepository.class); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/UserWithComplexId.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/UserWithComplexId.java index 606cca8647..c3bb9cb724 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/UserWithComplexId.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/UserWithComplexId.java @@ -15,9 +15,9 @@ */ package org.springframework.data.mongodb.repository; +import org.jspecify.annotations.Nullable; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VectorSearchTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VectorSearchTests.java new file mode 100644 index 0000000000..a224481da1 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VectorSearchTests.java @@ -0,0 +1,285 @@ +/* + * 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.mongodb.repository; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.SearchResult; +import org.springframework.data.domain.SearchResults; +import org.springframework.data.domain.Similarity; +import org.springframework.data.domain.Vector; +import org.springframework.data.mongodb.core.TestMongoConfiguration; +import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation; +import org.springframework.data.mongodb.core.index.VectorIndex; +import org.springframework.data.mongodb.core.index.VectorIndex.SimilarityFunction; +import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; +import org.springframework.data.mongodb.test.util.AtlasContainer; +import org.springframework.data.mongodb.test.util.MongoTestTemplate; +import org.springframework.data.repository.CrudRepository; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.mongodb.MongoDBAtlasLocalContainer; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; + +/** + * Integration tests using Vector Search and Vector Indexes through local MongoDB Atlas. + * + * @author Christoph Strobl + * @author Mark Paluch + */ +@Testcontainers(disabledWithoutDocker = true) +@SpringJUnitConfig(classes = { VectorSearchTests.Config.class }) +public class VectorSearchTests { + + Vector VECTOR = Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f); + + private static final MongoDBAtlasLocalContainer atlasLocal = AtlasContainer.bestMatch().withReuse(true); + private static final String COLLECTION_NAME = "collection-1"; + + static MongoClient client; + static MongoTestTemplate template; + + @Autowired VectorSearchRepository repository; + + @EnableMongoRepositories( + includeFilters = { + @ComponentScan.Filter(value = VectorSearchRepository.class, type = FilterType.ASSIGNABLE_TYPE) }, + considerNestedRepositories = true) + static class Config extends TestMongoConfiguration { + + @Override + public String getDatabaseName() { + return "vector-search-tests"; + } + + @Override + public MongoClient mongoClient() { + return MongoClients.create(atlasLocal.getConnectionString()); + } + } + + @BeforeAll + static void beforeAll() throws InterruptedException { + + atlasLocal.start(); + + client = MongoClients.create(atlasLocal.getConnectionString()); + template = new MongoTestTemplate(client, "vector-search-tests"); + + template.remove(WithVectorFields.class).all(); + initDocuments(); + initIndexes(); + + Thread.sleep(500); // just wait a little or the index will be broken + } + + @Test + void shouldSearchEnnWithAnnotatedFilter() { + + SearchResults results = repository.searchAnnotated("de", VECTOR, + Score.of(0.4), Limit.of(10)); + + assertThat(results).extracting(SearchResult::getScore).hasOnlyElementsOfType(Similarity.class); + assertThat(results).hasSize(3); + } + + @Test + void shouldSearchEnnWithDerivedFilter() { + + SearchResults results = repository.searchCosineByCountryAndEmbeddingNear("de", VECTOR, + Similarity.of(0.98), + Limit.of(10)); + + assertThat(results).extracting(SearchResult::getScore).hasOnlyElementsOfType(Similarity.class); + assertThat(results).hasSize(2).extracting(SearchResult::getContent).extracting(WithVectorFields::getCountry) + .containsOnly("de", "de"); + + assertThat(results).extracting(SearchResult::getContent).extracting(WithVectorFields::getDescription) + .containsExactlyInAnyOrder("two", "one"); + } + + @Test + void shouldSearchEnnWithDerivedFilterWithoutScore() { + + SearchResults de = repository.searchCosineByCountryAndEmbeddingNear("de", VECTOR, + Similarity.of(0.4), Limit.of(10)); + + assertThat(de).hasSizeGreaterThanOrEqualTo(2); + + assertThat(repository.searchCosineByCountryAndEmbeddingNear("de", VECTOR, Similarity.of(0.999), Limit.of(10))) + .hasSize(1); + } + + @Test + void shouldSearchAsListEnnWithDerivedFilterWithoutScore() { + + List de = repository.searchAsListByCountryAndEmbeddingNear("de", VECTOR, Limit.of(10)); + + assertThat(de).hasOnlyElementsOfType(WithVectorFields.class); + } + + @Test + void shouldSearchEuclideanWithDerivedFilter() { + + SearchResults results = repository.searchEuclideanByCountryAndEmbeddingNear("de", VECTOR, + Limit.of(2)); + + assertThat(results).hasSize(2).extracting(SearchResult::getContent).extracting(WithVectorFields::getCountry) + .containsOnly("de", "de"); + + assertThat(results).extracting(SearchResult::getContent).extracting(WithVectorFields::getDescription) + .containsExactlyInAnyOrder("two", "one"); + } + + @Test + void shouldSearchEnnWithDerivedFilterWithin() { + + SearchResults results = repository.searchByCountryAndEmbeddingWithin("de", VECTOR, + Similarity.between(0.93, 0.98)); + + assertThat(results).hasSize(1); + for (SearchResult result : results) { + assertThat(result.getScore().getValue()).isBetween(0.93, 0.98); + } + } + + @Test + void shouldSearchEnnWithDerivedAndLimitedFilterWithin() { + + SearchResults results = repository.searchTop1ByCountryAndEmbeddingWithin("de", VECTOR, + Similarity.between(0.8, 1)); + + assertThat(results).hasSize(1); + + for (SearchResult result : results) { + assertThat(result.getScore().getValue()).isBetween(0.8, 1.0); + } + } + + static void initDocuments() { + + WithVectorFields w1 = new WithVectorFields("de", "one", Vector.of(0.1001f, 0.22345f, 0.33456f, 0.44567f, 0.55678f)); + WithVectorFields w2 = new WithVectorFields("de", "two", Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f)); + WithVectorFields w3 = new WithVectorFields("en", "three", + Vector.of(0.9001f, 0.82345f, 0.73456f, 0.64567f, 0.55678f)); + WithVectorFields w4 = new WithVectorFields("de", "four", + Vector.of(0.9001f, 0.92345f, 0.93456f, 0.94567f, 0.95678f)); + + template.insertAll(List.of(w1, w2, w3, w4)); + } + + static void initIndexes() { + + VectorIndex cosIndex = new VectorIndex("cos-index") + .addVector("embedding", it -> it.similarity(SimilarityFunction.COSINE).dimensions(5)).addFilter("country"); + + template.searchIndexOps(WithVectorFields.class).createIndex(cosIndex); + + VectorIndex euclideanIndex = new VectorIndex("euc-index") + .addVector("embedding", it -> it.similarity(SimilarityFunction.EUCLIDEAN).dimensions(5)).addFilter("country"); + + VectorIndex inner = new VectorIndex("ip-index") + .addVector("embedding", it -> it.similarity(SimilarityFunction.DOT_PRODUCT).dimensions(5)).addFilter("country"); + + template.searchIndexOps(WithVectorFields.class).createIndex(cosIndex); + template.searchIndexOps(WithVectorFields.class).createIndex(euclideanIndex); + template.searchIndexOps(WithVectorFields.class).createIndex(inner); + template.awaitIndexCreation(WithVectorFields.class, cosIndex.getName()); + template.awaitIndexCreation(WithVectorFields.class, euclideanIndex.getName()); + template.awaitIndexCreation(WithVectorFields.class, inner.getName()); + } + + interface VectorSearchRepository extends CrudRepository { + + @VectorSearch(indexName = "cos-index", filter = "{country: ?0}", numCandidates = "#{10+10}", + searchType = VectorSearchOperation.SearchType.ANN) + SearchResults searchAnnotated(String country, Vector vector, + Score distance, Limit limit); + + @VectorSearch(indexName = "cos-index") + SearchResults searchCosineByCountryAndEmbeddingNear(String country, Vector vector, + Score similarity, Limit limit); + + @VectorSearch(indexName = "cos-index") + List searchAsListByCountryAndEmbeddingNear(String country, Vector vector, Limit limit); + + @VectorSearch(indexName = "euc-index") + SearchResults searchEuclideanByCountryAndEmbeddingNear(String country, Vector vector, + Limit limit); + + @VectorSearch(indexName = "cos-index", limit = "10") + SearchResults searchByCountryAndEmbeddingWithin(String country, Vector vector, + Range distance); + + @VectorSearch(indexName = "cos-index") + SearchResults searchTop1ByCountryAndEmbeddingWithin(String country, Vector vector, + Range distance); + + } + + @org.springframework.data.mongodb.core.mapping.Document(COLLECTION_NAME) + static class WithVectorFields { + + String id; + String country; + String description; + + Vector embedding; + + public WithVectorFields(String country, String description, Vector embedding) { + this.country = country; + this.description = description; + this.embedding = embedding; + } + + public String getId() { + return id; + } + + public String getCountry() { + return country; + } + + public String getDescription() { + return description; + } + + public Vector getEmbedding() { + return embedding; + } + + @Override + public String toString() { + return "WithVectorFields{" + "id='" + id + '\'' + ", country='" + country + '\'' + ", description='" + description + + '\'' + '}'; + } + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPerson.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPerson.java index 294e4ea501..4f1adc714e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPerson.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPerson.java @@ -17,9 +17,9 @@ import java.util.Objects; +import org.jspecify.annotations.Nullable; import org.springframework.data.annotation.Version; import org.springframework.data.mongodb.core.mapping.Document; -import org.springframework.lang.Nullable; /** * @author Christoph Strobl @@ -48,8 +48,7 @@ public String getFirstname() { return this.firstname; } - @Nullable - public String getLastname() { + public @Nullable String getLastname() { return this.lastname; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPersonRepositoryIntegrationTests.java index f4e1e0282e..917a1094d8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPersonRepositoryIntegrationTests.java @@ -19,6 +19,7 @@ import org.bson.Document; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -34,7 +35,6 @@ import org.springframework.data.mongodb.test.util.MongoClientExtension; import org.springframework.data.mongodb.test.util.MongoTestUtils; import org.springframework.data.repository.CrudRepository; -import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotContributionIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotContributionIntegrationTests.java new file mode 100644 index 0000000000..b46b1dfb50 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotContributionIntegrationTests.java @@ -0,0 +1,109 @@ +/* + * 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.mongodb.repository.aot; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*; +import static org.mockito.Mockito.*; + +import example.aot.User; +import example.aot.UserRepository; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.GeneratedFiles; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.aot.ApplicationContextAotGenerator; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.InputStreamSource; +import org.springframework.data.aot.AotContext; +import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.mock.env.MockPropertySource; + +import com.mongodb.client.MongoClient; + +/** + * Integration tests for AOT processing of imperative repositories. + * + * @author Mark Paluch + */ +class AotContributionIntegrationTests { + + @EnableMongoRepositories(considerNestedRepositories = true, includeFilters = { + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = QuerydslUserRepository.class) }) + static class AotConfiguration extends AbstractMongoClientConfiguration { + + @Override + public MongoClient mongoClient() { + return mock(MongoClient.class); + } + + @Override + protected String getDatabaseName() { + return ""; + } + } + + interface QuerydslUserRepository extends UserRepository, QuerydslPredicateExecutor { + + } + + @Test // GH-3830 + void shouldGenerateMetadataForBaseRepositoryAndQuerydslFragment() throws IOException { + + TestGenerationContext generationContext = generate(AotConfiguration.class); + + InputStreamSource metadata = generationContext.getGeneratedFiles().getGeneratedFile(GeneratedFiles.Kind.RESOURCE, + QuerydslUserRepository.class.getName().replace('.', '/') + ".json"); + + InputStreamResource isr = new InputStreamResource(metadata); + String json = isr.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).isObject() // + .containsEntry("name", QuerydslUserRepository.class.getName()) // + .containsEntry("module", "MongoDB") // + .containsEntry("type", "IMPERATIVE"); + + assertThatJson(json).inPath("$.methods[?(@.name == 'findBy')].fragment").isArray().first().isObject() + .containsEntry("interface", "org.springframework.data.querydsl.QuerydslPredicateExecutor").containsEntry( + "fragment", "org.springframework.data.mongodb.repository.support.QuerydslMongoPredicateExecutor"); + + assertThatJson(json).inPath("$.methods[?(@.name == 'existsById')].fragment").isArray().first().isObject() + .containsEntry("fragment", "org.springframework.data.mongodb.repository.support.SimpleMongoRepository"); + } + + private static TestGenerationContext generate(Class... configurationClasses) { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.getEnvironment().getPropertySources() + .addFirst(new MockPropertySource().withProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "true")); + context.register(configurationClasses); + + ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator(); + + TestGenerationContext generationContext = new TestGenerationContext(); + generator.processAheadOfTime(context, generationContext); + return generationContext; + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java new file mode 100644 index 0000000000..eba08ecc2e --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java @@ -0,0 +1,130 @@ +/* + * 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.mongodb.repository.aot; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.util.ReflectionUtils; + +/** + * Test Configuration Support Class for generated AOT Repository Fragments based on a Repository Interface. + *

+ * This configuration generates the AOT repository, compiles sources and configures a BeanFactory to contain the AOT + * fragment. Additionally, the fragment is exposed through a {@code repositoryInterface} JDK proxy forwarding method + * invocations to the backing AOT fragment. Note that {@code repositoryInterface} is not a repository proxy. + * + * @author Christoph Strobl + */ +public class AotFragmentTestConfigurationSupport implements BeanFactoryPostProcessor { + + private final Class repositoryInterface; + private final TestMongoAotRepositoryContext repositoryContext; + + public AotFragmentTestConfigurationSupport(Class repositoryInterface) { + + this.repositoryInterface = repositoryInterface; + this.repositoryContext = new TestMongoAotRepositoryContext(repositoryInterface, null); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + + TestGenerationContext generationContext = new TestGenerationContext(repositoryInterface); + + new MongoRepositoryContributor(repositoryContext).contribute(generationContext); + + AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder + .genericBeanDefinition( + repositoryInterface.getPackageName() + "." + repositoryInterface.getSimpleName() + "Impl__Aot") // + .addConstructorArgReference("mongoOperations") // + .addConstructorArgValue(getCreationContext(repositoryContext)).getBeanDefinition(); + + TestCompiler.forSystem().withCompilerOptions("-parameters").with(generationContext).compile(compiled -> { + beanFactory.setBeanClassLoader(compiled.getClassLoader()); + ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragment", aotGeneratedRepository); + }); + + BeanDefinition fragmentFacade = BeanDefinitionBuilder.rootBeanDefinition((Class) repositoryInterface, () -> { + + Object fragment = beanFactory.getBean("fragment"); + Object proxy = getFragmentFacadeProxy(fragment); + + return repositoryInterface.cast(proxy); + }).getBeanDefinition(); + + ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragmentFacade", fragmentFacade); + + beanFactory.registerSingleton("generationContext", generationContext); + } + + private Object getFragmentFacadeProxy(Object fragment) { + + return Proxy.newProxyInstance(repositoryInterface.getClassLoader(), new Class[] { repositoryInterface }, + (p, method, args) -> { + + Method target = ReflectionUtils.findMethod(fragment.getClass(), method.getName(), method.getParameterTypes()); + + if (target == null) { + throw new NoSuchMethodException("Method [%s] is not implemented by [%s]".formatted(method, target)); + } + + try { + return target.invoke(fragment, args); + } catch (ReflectiveOperationException e) { + ReflectionUtils.handleReflectionException(e); + } + + return null; + }); + } + + private RepositoryFactoryBeanSupport.FragmentCreationContext getCreationContext( + TestMongoAotRepositoryContext repositoryContext) { + + return new RepositoryFactoryBeanSupport.FragmentCreationContext() { + + @Override + public RepositoryMetadata getRepositoryMetadata() { + return repositoryContext.getRepositoryInformation(); + } + + @Override + public ValueExpressionDelegate getValueExpressionDelegate() { + return ValueExpressionDelegate.create(); + } + + @Override + public ProjectionFactory getProjectionFactory() { + return new SpelAwareProxyProjectionFactory(); + } + }; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java new file mode 100644 index 0000000000..1c9796ead6 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java @@ -0,0 +1,705 @@ +/* + * 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.mongodb.repository.aot; + +import static org.assertj.core.api.Assertions.*; + +import example.aot.User; +import example.aot.UserProjection; +import example.aot.UserRepository; +import example.aot.UserRepository.UserAggregate; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +import org.bson.Document; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.KeysetScrollPosition; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.OffsetScrollPosition; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Window; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.test.util.Client; +import org.springframework.data.mongodb.test.util.MongoClientExtension; +import org.springframework.data.mongodb.test.util.MongoTestUtils; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.util.StringUtils; + +import com.mongodb.client.MongoClient; + +/** + * Integration tests for the {@link UserRepository} AOT fragment. + * + * @author Christoph Strobl + * @author Mark Paluch + */ +@ExtendWith(MongoClientExtension.class) +@SpringJUnitConfig(classes = MongoRepositoryContributorTests.MongoRepositoryContributorConfiguration.class) +class MongoRepositoryContributorTests { + + private static final String DB_NAME = "aot-repo-tests"; + + @Client static MongoClient client; + @Autowired UserRepository fragment; + + @Configuration + static class MongoRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport { + + public MongoRepositoryContributorConfiguration() { + super(UserRepository.class); + } + + @Bean + MongoOperations mongoOperations() { + return new MongoTemplate(client, DB_NAME); + } + } + + @BeforeEach + void beforeEach() { + + MongoTestUtils.flushCollection(DB_NAME, "user", client); + initUsers(); + } + + @Test + void testFindDerivedFinderSingleEntity() { + + User user = fragment.findOneByUsername("yoda"); + assertThat(user).isNotNull().extracting(User::getUsername).isEqualTo("yoda"); + } + + @Test + void testFindDerivedFinderOptionalEntity() { + + Optional user = fragment.findOptionalOneByUsername("yoda"); + assertThat(user).isNotNull().containsInstanceOf(User.class) + .hasValueSatisfying(it -> assertThat(it).extracting(User::getUsername).isEqualTo("yoda")); + } + + @Test + void testDerivedCount() { + + assertThat(fragment.countUsersByLastname("Skywalker")).isEqualTo(2L); + assertThat(fragment.countUsersAsIntByLastname("Skywalker")).isEqualTo(2); + } + + @Test + void testDerivedExists() { + + assertThat(fragment.existsUserByLastname("Skywalker")).isTrue(); + } + + @Test + void testDerivedFinderWithoutArguments() { + + List users = fragment.findUserNoArgumentsBy(); + assertThat(users).hasSize(7).hasOnlyElementsOfType(User.class); + } + + @Test + void testCountWorksAsExpected() { + + Long value = fragment.countUsersByLastname("Skywalker"); + assertThat(value).isEqualTo(2L); + } + + @Test + void testDerivedFinderReturningList() { + + List users = fragment.findByLastnameStartingWith("S"); + assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("luke", "vader", "kylo", "han"); + } + + @Test + void testEndingWith() { + + List users = fragment.findByLastnameEndsWith("er"); + assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("luke", "vader"); + } + + @Test + void testLike() { + + List users = fragment.findByFirstnameLike("ei"); + assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("leia"); + } + + @Test + void testNotLike() { + + List users = fragment.findByFirstnameNotLike("ei"); + assertThat(users).extracting(User::getUsername).isNotEmpty().doesNotContain("leia"); + } + + @Test + void testIn() { + + List users = fragment.findByUsernameIn(List.of("chewbacca", "kylo")); + assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("chewbacca", "kylo"); + } + + @Test + void testNotIn() { + + List users = fragment.findByUsernameNotIn(List.of("chewbacca", "kylo")); + assertThat(users).extracting(User::getUsername).isNotEmpty().doesNotContain("chewbacca", "kylo"); + } + + @Test + void testAnd() { + + List users = fragment.findByFirstnameAndLastname("Han", "Solo"); + assertThat(users).extracting(User::getUsername).containsExactly("han"); + } + + @Test + void testOr() { + + List users = fragment.findByFirstnameOrLastname("Han", "Skywalker"); + assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "vader", "luke"); + } + + @Test + void testBetween() { + + List users = fragment.findByVisitsBetween(10, 100); + assertThat(users).extracting(User::getUsername).containsExactly("vader"); + } + + @Test + void testTimeValue() { + + List users = fragment.findByLastSeenGreaterThan(Instant.parse("2025-01-01T00:00:00.000Z")); + assertThat(users).extracting(User::getUsername).containsExactly("luke"); + } + + @Test + void testNot() { + + List users = fragment.findByLastnameNot("Skywalker"); + assertThat(users).extracting(User::getUsername).isNotEmpty().doesNotContain("luke", "vader"); + } + + @Test + void testExistsCriteria() { + + List users = fragment.findByVisitsExists(false); + assertThat(users).extracting(User::getUsername).contains("kylo"); + } + + @Test + void testLimitedDerivedFinder() { + + List users = fragment.findTop2ByLastnameStartingWith("S"); + assertThat(users).hasSize(2); + } + + @Test + void testSortedDerivedFinder() { + + List users = fragment.findByLastnameStartingWithOrderByUsername("S"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); + } + + @Test + void testDerivedFinderWithLimitArgument() { + + List users = fragment.findByLastnameStartingWith("S", Limit.of(2)); + assertThat(users).hasSize(2); + } + + @Test + void testDerivedFinderWithSort() { + + List users = fragment.findByLastnameStartingWith("S", Sort.by("username")); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); + } + + @Test + void testDerivedFinderWithSortAndLimit() { + + List users = fragment.findByLastnameStartingWith("S", Sort.by("username"), Limit.of(2)); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testDerivedFinderReturningListWithPageable() { + + List users = fragment.findByLastnameStartingWith("S", PageRequest.of(0, 2, Sort.by("username"))); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testDerivedFinderReturningPage() { + + Page page = fragment.findPageOfUsersByLastnameStartingWith("S", PageRequest.of(0, 2, Sort.by("username"))); + assertThat(page.getTotalElements()).isEqualTo(4); + assertThat(page.getSize()).isEqualTo(2); + assertThat(page.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testDerivedFinderReturningSlice() { + + Slice slice = fragment.findSliceOfUserByLastnameStartingWith("S", PageRequest.of(0, 2, Sort.by("username"))); + assertThat(slice.hasNext()).isTrue(); + assertThat(slice.getSize()).isEqualTo(2); + assertThat(slice.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testDerivedQueryReturningStream() { + + List results = fragment.streamByLastnameStartingWith("S", Sort.by("username"), Limit.of(2)).toList(); + + assertThat(results).hasSize(2); + assertThat(results).extracting(User::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testDerivedQueryReturningWindowByOffset() { + + Window window1 = fragment.findTop2WindowByLastnameStartingWithOrderByUsername("S", ScrollPosition.offset()); + assertThat(window1).extracting(User::getUsername).containsExactly("han", "kylo"); + assertThat(window1.positionAt(1)).isInstanceOf(OffsetScrollPosition.class); + + Window window2 = fragment.findTop2WindowByLastnameStartingWithOrderByUsername("S", window1.positionAt(1)); + assertThat(window2).extracting(User::getUsername).containsExactly("luke", "vader"); + } + + @Test + void testDerivedQueryReturningWindowByKeyset() { + + Window window1 = fragment.findTop2WindowByLastnameStartingWithOrderByUsername("S", ScrollPosition.keyset()); + assertThat(window1).extracting(User::getUsername).containsExactly("han", "kylo"); + assertThat(window1.positionAt(1)).isInstanceOf(KeysetScrollPosition.class); + + Window window2 = fragment.findTop2WindowByLastnameStartingWithOrderByUsername("S", window1.positionAt(1)); + assertThat(window2).extracting(User::getUsername).containsExactly("luke", "vader"); + } + + @Test + void testAnnotatedFinderReturningSingleValueWithQuery() { + + User user = fragment.findAnnotatedQueryByUsername("yoda"); + assertThat(user).isNotNull().extracting(User::getUsername).isEqualTo("yoda"); + } + + @Test + void testAnnotatedCount() { + + Long value = fragment.countAnnotatedQueryByLastname("Skywalker"); + assertThat(value).isEqualTo(2L); + } + + @Test + void testAnnotatedFinderReturningListWithQuery() { + + List users = fragment.findAnnotatedQueryByLastname("S"); + assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader"); + } + + @Test + void testAnnotatedMultilineFinderWithQuery() { + + List users = fragment.findAnnotatedMultilineQueryByLastname("S"); + assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader"); + } + + @Test + void testAnnotatedFinderWithQueryAndLimit() { + + List users = fragment.findAnnotatedQueryByLastname("S", Limit.of(2)); + assertThat(users).hasSize(2); + } + + @Test + void testAnnotatedFinderWithQueryAndSort() { + + List users = fragment.findAnnotatedQueryByLastname("S", Sort.by("username")); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); + } + + @Test + void testAnnotatedFinderWithQueryLimitAndSort() { + + List users = fragment.findAnnotatedQueryByLastname("S", Limit.of(2), Sort.by("username")); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testAnnotatedFinderReturningListWithPageable() { + + List users = fragment.findAnnotatedQueryByLastname("S", PageRequest.of(0, 2, Sort.by("username"))); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testAnnotatedFinderReturningPage() { + + Page page = fragment.findAnnotatedQueryPageOfUsersByLastname("S", PageRequest.of(0, 2, Sort.by("username"))); + assertThat(page.getTotalElements()).isEqualTo(4); + assertThat(page.getSize()).isEqualTo(2); + assertThat(page.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testAnnotatedFinderReturningSlice() { + + Slice slice = fragment.findAnnotatedQuerySliceOfUsersByLastname("S", + PageRequest.of(0, 2, Sort.by("username"))); + assertThat(slice.hasNext()).isTrue(); + assertThat(slice.getSize()).isEqualTo(2); + assertThat(slice.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testDeleteSingle() { + + User result = fragment.deleteByUsername("yoda"); + + assertThat(result).isNotNull().extracting(User::getUsername).isEqualTo("yoda"); + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(6L); + } + + @Test + void testDeleteSingleAnnotatedQuery() { + + User result = fragment.deleteAnnotatedQueryByUsername("yoda"); + + assertThat(result).isNotNull().extracting(User::getUsername).isEqualTo("yoda"); + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(6L); + } + + @Test + void testDerivedDeleteMultipleReturningDeleteCount() { + + Long result = fragment.deleteByLastnameStartingWith("S"); + + assertThat(result).isEqualTo(4L); + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L); + } + + @Test + void testDerivedDeleteMultipleReturningDeleteCountAnnotatedQuery() { + + Long result = fragment.deleteAnnotatedQueryByLastnameStartingWith("S"); + + assertThat(result).isEqualTo(4L); + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L); + } + + @Test + void testDerivedDeleteMultipleReturningDeleted() { + + List result = fragment.deleteUsersByLastnameStartingWith("S"); + + assertThat(result).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader"); + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L); + } + + @Test + void testDerivedDeleteMultipleReturningDeletedAnnotatedQuery() { + + List result = fragment.deleteUsersAnnotatedQueryByLastnameStartingWith("S"); + + assertThat(result).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader"); + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L); + } + + @Test + void testDerivedFinderWithAnnotatedSort() { + + List users = fragment.findWithAnnotatedSortByLastnameStartingWith("S"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); + } + + @Test + void testDerivedFinderWithAnnotatedFieldsProjection() { + + List users = fragment.findWithAnnotatedFieldsProjectionByLastnameStartingWith("S"); + assertThat(users).allMatch( + user -> StringUtils.hasText(user.getUsername()) && user.getLastname() == null && user.getFirstname() == null); + } + + @Test + void testReadPreferenceAppliedToQuery() { + + // check if it fails when trying to parse the read preference to indicate it would get applied + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> fragment.findWithReadPreferenceByUsername("S")) + .withMessageContaining("No match for read preference"); + } + + @Test + void testDerivedFinderReturningListOfProjections() { + + List users = fragment.findUserProjectionByLastnameStartingWith("S"); + assertThat(users).extracting(UserProjection::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader"); + } + + @Test + void testDerivedFinderReturningPageOfProjections() { + + Page users = fragment.findUserProjectionByLastnameStartingWith("S", + PageRequest.of(0, 2, Sort.by("username"))); + assertThat(users).extracting(UserProjection::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testDerivedFinderReturningPageOfDynamicProjections() { + + Page users = fragment.findUserProjectionByLastnameStartingWith("S", + PageRequest.of(0, 2, Sort.by("username")), UserProjection.class); + assertThat(users).extracting(UserProjection::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testUpdateWithDerivedQuery() { + + int modifiedCount = fragment.findUserAndIncrementVisitsByLastname("Organa", 42); + + assertThat(modifiedCount).isOne(); + assertThat(client.getDatabase(DB_NAME).getCollection("user").find(new Document("_id", "id-2")).first().get("visits", + Integer.class)).isEqualTo(42); + } + + @Test + void testUpdateWithAnnotatedQuery() { + + int modifiedCount = fragment.updateAllByLastname("Organa", 42); + + assertThat(modifiedCount).isOne(); + assertThat(client.getDatabase(DB_NAME).getCollection("user").find(new Document("_id", "id-2")).first().get("visits", + Integer.class)).isEqualTo(42); + } + + @Test + void testAggregationPipelineUpdate() { + + fragment.findAndIncrementVisitsViaPipelineByLastname("Organa", 42); + + assertThat(client.getDatabase(DB_NAME).getCollection("user").find(new Document("_id", "id-2")).first().get("visits", + Integer.class)).isEqualTo(42); + } + + @Test + void testAggregationWithExtractedSimpleResults() { + + List allLastnames = fragment.findAllLastnames(); + assertThat(allLastnames).containsExactlyInAnyOrder("Skywalker", "Solo", "Organa", "Solo", "Skywalker"); + } + + @Test + void testAggregationWithProjectedResults() { + + List allLastnames = fragment.groupByLastnameAnd("first_name"); + assertThat(allLastnames).containsExactlyInAnyOrder(// + new UserAggregate("Skywalker", List.of("Anakin", "Luke")), // + new UserAggregate("Organa", List.of("Leia")), // + new UserAggregate("Solo", List.of("Han", "Ben"))); + } + + @Test + void testAggregationWithProjectedResultsLimitedByPageable() { + + List allLastnames = fragment.groupByLastnameAnd("first_name", PageRequest.of(1, 1, Sort.by("_id"))); + assertThat(allLastnames).containsExactly(// + new UserAggregate("Skywalker", List.of("Anakin", "Luke")) // + ); + } + + @Test + void testAggregationWithProjectedResultsAsPage() { + + Slice allLastnames = fragment.groupByLastnameAndReturnPage("first_name", + PageRequest.of(1, 1, Sort.by("_id"))); + assertThat(allLastnames.hasPrevious()).isTrue(); + assertThat(allLastnames.hasNext()).isTrue(); + assertThat(allLastnames.getContent()).containsExactly(// + new UserAggregate("Skywalker", List.of("Anakin", "Luke")) // + ); + } + + @Test + void testAggregationWithProjectedResultsWrappedInAggregationResults() { + + AggregationResults allLastnames = fragment.groupByLastnameAndAsAggregationResults("first_name"); + assertThat(allLastnames.getMappedResults()).containsExactlyInAnyOrder(// + new UserAggregate("Skywalker", List.of("Anakin", "Luke")), // + new UserAggregate("Organa", List.of("Leia")), // + new UserAggregate("Solo", List.of("Han", "Ben"))); + } + + @Test + void testAggregationStreamWithProjectedResultsWrappedInAggregationResults() { + + List allLastnames = fragment.streamGroupByLastnameAndAsAggregationResults("first_name").toList(); + assertThat(allLastnames).containsExactlyInAnyOrder(// + new UserAggregate("Skywalker", List.of("Anakin", "Luke")), // + new UserAggregate("Organa", List.of("Leia")), // + new UserAggregate("Solo", List.of("Han", "Ben"))); + } + + @Test + void testAggregationWithSingleResultExtraction() { + assertThat(fragment.sumPosts()).isEqualTo(5); + } + + @Test + void testAggregationWithHint() { + assertThatException().isThrownBy(() -> fragment.findAllLastnamesUsingIndex()) + .withMessageContaining("hint provided does not correspond to an existing index"); + } + + @Test + void testAggregationWithReadPreference() { + assertThatException().isThrownBy(() -> fragment.findAllLastnamesWithReadPreference()) + .withMessageContaining("No match for read preference"); + } + + @Test + void testAggregationWithCollation() { + assertThatException().isThrownBy(() -> fragment.findAllLastnamesWithCollation()) + .withMessageContaining("'locale' is invalid"); + } + + private static void initUsers() { + + Document luke = Document.parse(""" + { + "_id": "id-1", + "username": "luke", + "first_name": "Luke", + "last_name": "Skywalker", + "visits" : 2, + "lastSeen" : { + "$date": "2025-04-01T00:00:00.000Z" + }, + "posts": [ + { + "message": "I have a bad feeling about this.", + "date": { + "$date": "2025-01-15T12:50:33.855Z" + } + } + ], + "_class": "example.springdata.aot.User" + }"""); + + Document leia = Document.parse(""" + { + "_id": "id-2", + "username": "leia", + "first_name": "Leia", + "last_name": "Organa", + "_class": "example.springdata.aot.User" + }"""); + + Document han = Document.parse(""" + { + "_id": "id-3", + "username": "han", + "first_name": "Han", + "last_name": "Solo", + "posts": [ + { + "message": "It's the ship that made the Kessel Run in less than 12 Parsecs.", + "date": { + "$date": "2025-01-15T13:30:33.855Z" + } + } + ], + "_class": "example.springdata.aot.User" + }"""); + + Document chwebacca = Document.parse(""" + { + "_id": "id-4", + "username": "chewbacca", + "lastSeen" : { + "$date": "2025-01-01T00:00:00.000Z" + }, + "_class": "example.springdata.aot.User" + }"""); + + Document yoda = Document.parse( + """ + { + "_id": "id-5", + "username": "yoda", + "visits" : 1000, + "posts": [ + { + "message": "Do. Or do not. There is no try.", + "date": { + "$date": "2025-01-15T13:09:33.855Z" + } + }, + { + "message": "Decide you must, how to serve them best. If you leave now, help them you could; but you would destroy all for which they have fought, and suffered.", + "date": { + "$date": "2025-01-15T13:53:33.855Z" + } + } + ] + }"""); + + Document vader = Document.parse(""" + { + "_id": "id-6", + "username": "vader", + "first_name": "Anakin", + "last_name": "Skywalker", + "visits" : 50, + "posts": [ + { + "message": "I am your father", + "date": { + "$date": "2025-01-15T13:46:33.855Z" + } + } + ] + }"""); + + Document kylo = Document.parse(""" + { + "_id": "id-7", + "username": "kylo", + "first_name": "Ben", + "last_name": "Solo" + } + """); + + client.getDatabase(DB_NAME).getCollection("user") + .insertMany(List.of(luke, leia, han, chwebacca, yoda, vader, kylo)); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorUnitTests.java new file mode 100644 index 0000000000..e53b3ae679 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorUnitTests.java @@ -0,0 +1,90 @@ +/* + * 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.mongodb.repository.aot; + +import static org.mockito.Mockito.*; +import static org.springframework.data.mongodb.test.util.Assertions.*; + +import example.aot.User; +import example.aot.UserRepository; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.aot.generate.GeneratedFiles; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.InputStreamSource; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.repository.Meta; +import org.springframework.data.mongodb.test.util.MongoClientExtension; +import org.springframework.data.repository.CrudRepository; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +/** + * Unit tests for the {@link UserRepository} fragment sources via {@link MongoRepositoryContributor}. + * + * @author Mark Paluch + */ +@ExtendWith(MongoClientExtension.class) +@SpringJUnitConfig(classes = MongoRepositoryContributorUnitTests.MongoRepositoryContributorConfiguration.class) +class MongoRepositoryContributorUnitTests { + + @Configuration + static class MongoRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport { + + public MongoRepositoryContributorConfiguration() { + super(MetaUserRepository.class); + } + + @Bean + MongoOperations mongoOperations() { + return mock(MongoOperations.class); + } + + } + + @Autowired TestGenerationContext generationContext; + + @Test + void shouldConsiderMetaAnnotation() throws IOException { + + InputStreamSource aotFragment = generationContext.getGeneratedFiles().getGeneratedFile(GeneratedFiles.Kind.SOURCE, + MetaUserRepository.class.getPackageName().replace('.', '/') + "/MetaUserRepositoryImpl__Aot.java"); + + String content = new InputStreamResource(aotFragment).getContentAsString(StandardCharsets.UTF_8); + + assertThat(content).contains("filterQuery.maxTimeMsec(555)"); + assertThat(content).contains("filterQuery.cursorBatchSize(1234)"); + assertThat(content).contains("filterQuery.comment(\"foo\")"); + } + + interface MetaUserRepository extends CrudRepository { + + @Meta + User findAllByLastname(String lastname); + + @Meta(cursorBatchSize = 1234, comment = "foo", maxExecutionTimeMs = 555) + User findWithMetaAllByLastname(String lastname); + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java new file mode 100644 index 0000000000..aa069a2710 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java @@ -0,0 +1,192 @@ +/* + * 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.mongodb.repository.aot; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import example.aot.UserRepository; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.test.util.MongoClientExtension; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +/** + * Integration tests for the {@link UserRepository} JSON metadata via {@link MongoRepositoryContributor}. + * + * @author Mark Paluch + */ +@ExtendWith(MongoClientExtension.class) +@SpringJUnitConfig(classes = MongoRepositoryMetadataTests.MongoRepositoryContributorConfiguration.class) +class MongoRepositoryMetadataTests { + + @Configuration + static class MongoRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport { + + public MongoRepositoryContributorConfiguration() { + super(UserRepository.class); + } + + @Bean + MongoOperations mongoOperations() { + return mock(MongoOperations.class); + } + + } + + @Autowired AbstractApplicationContext context; + + @Test // GH-4964 + void shouldDocumentBase() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).isObject() // + .containsEntry("name", UserRepository.class.getName()) // + .containsEntry("module", "MongoDB") // + .containsEntry("type", "IMPERATIVE"); + } + + @Test // GH-4964 + void shouldDocumentDerivedQuery() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'countUsersByLastname')].query").isArray().element(0).isObject() + .containsEntry("filter", "{'lastname':?0}"); + } + + @Test // GH-4964 + void shouldDocumentSortedQuery() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'findByLastnameStartingWithOrderByUsername')].query") // + .isArray().element(0).isObject() // + .containsEntry("filter", "{'lastname':{'$regex':/^\\Q?0\\E/}}") + .containsEntry("sort", "{'username':{'$numberInt':'1'}}"); + } + + @Test // GH-4964 + void shouldDocumentPagedQuery() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'findPageOfUsersByLastnameStartingWith')].query").isArray() + .element(0).isObject().containsEntry("filter", "{'lastname':{'$regex':/^\\Q?0\\E/}}"); + } + + @Test // GH-4964 + @Disabled("No support for expressions yet") + void shouldDocumentQueryWithExpression() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'findValueExpressionNamedByEmailAddress')].query").isArray() + .first().isObject().containsEntry("query", "select u from User u where u.emailAddress = :__$synthetic$__1"); + } + + @Test // GH-4964 + void shouldDocumentAggregation() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'findAllLastnames')].query").isArray().element(0).isObject() + .containsEntry("pipeline", + "[{ '$match' : { 'last_name' : { '$ne' : null } } }, { '$project': { '_id' : '$last_name' } }]"); + } + + @Test // GH-4964 + void shouldDocumentPipelineUpdate() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'findAndIncrementVisitsViaPipelineByLastname')].query").isArray() + .element(0).isObject().containsEntry("filter", "{'lastname':?0}").containsEntry("update-pipeline", + "[{ '$set' : { 'visits' : { '$ifNull' : [ {'$add' : [ '$visits', ?1 ] }, ?1 ] } } }]"); + } + + @Test // GH-4964 + void shouldDocumentBaseFragment() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'existsById')].fragment").isArray().first().isObject() + .containsEntry("fragment", "org.springframework.data.mongodb.repository.support.SimpleMongoRepository"); + } + + private Resource getResource() { + + String location = UserRepository.class.getPackageName().replace('.', '/') + "/" + + UserRepository.class.getSimpleName() + ".json"; + return new UrlResource(context.getBeanFactory().getBeanClassLoader().getResource(location)); + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/ReactiveAotContributionIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/ReactiveAotContributionIntegrationTests.java new file mode 100644 index 0000000000..24b89345a8 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/ReactiveAotContributionIntegrationTests.java @@ -0,0 +1,116 @@ +/* + * 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.mongodb.repository.aot; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.mockito.Mockito.mock; + +import example.aot.User; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; +import org.springframework.aot.generate.GeneratedFiles; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.aot.ApplicationContextAotGenerator; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.InputStreamSource; +import org.springframework.data.aot.AotContext; +import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; +import org.springframework.data.repository.CrudRepository; +import org.springframework.mock.env.MockPropertySource; + +import com.mongodb.client.MongoClient; + +/** + * Integration tests for AOT processing of reactive repositories. + * + * @author Mark Paluch + */ +class ReactiveAotContributionIntegrationTests { + + @EnableReactiveMongoRepositories(considerNestedRepositories = true, includeFilters = { + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = ReactiveQuerydslUserRepository.class) }) + static class AotConfiguration extends AbstractMongoClientConfiguration { + + @Override + public MongoClient mongoClient() { + return mock(MongoClient.class); + } + + @Override + protected String getDatabaseName() { + return ""; + } + } + + interface ReactiveQuerydslUserRepository + extends CrudRepository, ReactiveQuerydslPredicateExecutor { + + Flux findUserNoArgumentsBy(); + + Mono findOneByUsername(String username); + + } + + @Test // GH-4964 + void shouldGenerateMetadataForBaseRepositoryAndQuerydslFragment() throws IOException { + + TestGenerationContext generationContext = generate(AotConfiguration.class); + + InputStreamSource metadata = generationContext.getGeneratedFiles().getGeneratedFile(GeneratedFiles.Kind.RESOURCE, + ReactiveQuerydslUserRepository.class.getName().replace('.', '/') + ".json"); + + InputStreamResource isr = new InputStreamResource(metadata); + String json = isr.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).isObject() // + .containsEntry("name", ReactiveQuerydslUserRepository.class.getName()) // + .containsEntry("module", "MongoDB") // + .containsEntry("type", "REACTIVE"); + + assertThatJson(json).inPath("$.methods[?(@.name == 'findBy')].fragment").isArray().first().isObject() + .containsEntry("interface", "org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor") + .containsEntry("fragment", + "org.springframework.data.mongodb.repository.support.ReactiveQuerydslMongoPredicateExecutor"); + + assertThatJson(json).inPath("$.methods[?(@.name == 'existsById')].fragment").isArray().first().isObject() + .containsEntry("fragment", "org.springframework.data.mongodb.repository.support.SimpleReactiveMongoRepository"); + } + + private static TestGenerationContext generate(Class... configurationClasses) { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.getEnvironment().getPropertySources() + .addFirst(new MockPropertySource().withProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "true")); + context.register(configurationClasses); + + ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator(); + + TestGenerationContext generationContext = new TestGenerationContext(); + generator.processAheadOfTime(context, generationContext); + return generationContext; + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/StubRepositoryInformation.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/StubRepositoryInformation.java new file mode 100644 index 0000000000..7092848c33 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/StubRepositoryInformation.java @@ -0,0 +1,140 @@ +/* + * 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.mongodb.repository.aot; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Set; + +import org.jspecify.annotations.Nullable; +import org.springframework.data.mongodb.repository.support.SimpleMongoRepository; +import org.springframework.data.repository.core.CrudMethods; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.util.TypeInformation; + +/** + * @author Christoph Strobl + */ +class StubRepositoryInformation implements RepositoryInformation { + + private final RepositoryMetadata metadata; + private final RepositoryComposition baseComposition; + + public StubRepositoryInformation(Class repositoryInterface, @Nullable RepositoryComposition composition) { + + this.metadata = AbstractRepositoryMetadata.getMetadata(repositoryInterface); + this.baseComposition = composition != null ? composition + : RepositoryComposition.of(RepositoryFragment.structural(SimpleMongoRepository.class)); + } + + @Override + public TypeInformation getIdTypeInformation() { + return metadata.getIdTypeInformation(); + } + + @Override + public TypeInformation getDomainTypeInformation() { + return metadata.getDomainTypeInformation(); + } + + @Override + public Class getRepositoryInterface() { + return metadata.getRepositoryInterface(); + } + + @Override + public TypeInformation getReturnType(Method method) { + return metadata.getReturnType(method); + } + + @Override + public Class getReturnedDomainClass(Method method) { + return metadata.getReturnedDomainClass(method); + } + + @Override + public TypeInformation getReturnedDomainTypeInformation(Method method) { + return metadata.getReturnedDomainTypeInformation(method); + } + + @Override + public CrudMethods getCrudMethods() { + return metadata.getCrudMethods(); + } + + @Override + public boolean isPagingRepository() { + return false; + } + + @Override + public Set> getAlternativeDomainTypes() { + return null; + } + + @Override + public boolean isReactiveRepository() { + return false; + } + + @Override + public Set> getFragments() { + return null; + } + + @Override + public boolean isBaseClassMethod(Method method) { + return baseComposition.findMethod(method).isPresent(); + } + + @Override + public boolean isCustomMethod(Method method) { + return false; + } + + @Override + public boolean isQueryMethod(Method method) { + if (isBaseClassMethod(method)) { + return false; + } + + return true; + } + + @Override + public List getQueryMethods() { + return null; + } + + @Override + public Class getRepositoryBaseClass() { + return SimpleMongoRepository.class; + } + + @Override + public Method getTargetClassMethod(Method method) { + return null; + } + + @Override + public RepositoryComposition getRepositoryComposition() { + return baseComposition; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java new file mode 100644 index 0000000000..5f470dd550 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java @@ -0,0 +1,124 @@ +/* + * 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.mongodb.repository.aot; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Set; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.test.tools.ClassFile; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.config.RepositoryConfigurationSource; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.support.RepositoryComposition; + +/** + * @author Christoph Strobl + */ +public class TestMongoAotRepositoryContext implements AotRepositoryContext { + + private final StubRepositoryInformation repositoryInformation; + private final Environment environment = new StandardEnvironment(); + + public TestMongoAotRepositoryContext(Class repositoryInterface, @Nullable RepositoryComposition composition) { + this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition); + } + + @Override + public ConfigurableListableBeanFactory getBeanFactory() { + return null; + } + + @Override + public TypeIntrospector introspectType(String typeName) { + return null; + } + + @Override + public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { + return null; + } + + @Override + public String getBeanName() { + return "dummyRepository"; + } + + @Override + public String getModuleName() { + return "MongoDB"; + } + + @Override + public RepositoryConfigurationSource getConfigurationSource() { + return null; + } + + @Override + public Set getBasePackages() { + return Set.of("org.springframework.data.dummy.repository.aot"); + } + + @Override + public Set> getIdentifyingAnnotations() { + return Set.of(Document.class); + } + + @Override + public RepositoryInformation getRepositoryInformation() { + return repositoryInformation; + } + + @Override + public Set> getResolvedAnnotations() { + return Set.of(); + } + + @Override + public Set> getResolvedTypes() { + return Set.of(); + } + + public List getRequiredContextFiles() { + return List.of(classFileForType(repositoryInformation.getRepositoryBaseClass())); + } + + static ClassFile classFileForType(Class type) { + + String name = type.getName(); + ClassPathResource cpr = new ClassPathResource(name.replaceAll("\\.", "/") + ".class"); + + try { + return ClassFile.of(name, cpr.getContentAsByteArray()); + } catch (IOException e) { + throw new IllegalArgumentException("Cannot open [%s].".formatted(cpr.getPath())); + } + } + + @Override + public Environment getEnvironment() { + return environment; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtensionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtensionUnitTests.java index f613beb6d5..a222deca39 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtensionUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtensionUnitTests.java @@ -27,7 +27,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.StandardAnnotationMetadata; +import org.springframework.core.type.AnnotationMetadata; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.repository.Repository; @@ -43,13 +43,13 @@ */ public class MongoRepositoryConfigurationExtensionUnitTests { - StandardAnnotationMetadata metadata = new StandardAnnotationMetadata(Config.class, true); + AnnotationMetadata metadata = AnnotationMetadata.introspect(Config.class); ResourceLoader loader = new PathMatchingResourcePatternResolver(); Environment environment = new StandardEnvironment(); BeanDefinitionRegistry registry = new DefaultListableBeanFactory(); RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata, - EnableMongoRepositories.class, loader, environment, registry); + EnableMongoRepositories.class, loader, environment, registry, null); @Test // DATAMONGO-1009 public void isStrictMatchIfDomainTypeIsAnnotatedWithDocument() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtensionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtensionUnitTests.java index 45ecba992f..2b52204f74 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtensionUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtensionUnitTests.java @@ -27,7 +27,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.StandardAnnotationMetadata; +import org.springframework.core.type.AnnotationMetadata; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; @@ -43,13 +43,13 @@ */ public class ReactiveMongoRepositoryConfigurationExtensionUnitTests { - StandardAnnotationMetadata metadata = new StandardAnnotationMetadata(Config.class, true); + AnnotationMetadata metadata = AnnotationMetadata.introspect(Config.class); ResourceLoader loader = new PathMatchingResourcePatternResolver(); Environment environment = new StandardEnvironment(); BeanDefinitionRegistry registry = new DefaultListableBeanFactory(); RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata, - EnableReactiveMongoRepositories.class, loader, environment, registry); + EnableReactiveMongoRepositories.class, loader, environment, registry, null); @Test // DATAMONGO-1444 public void isStrictMatchIfDomainTypeIsAnnotatedWithDocument() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java index ea3c9ad023..dad28ae5aa 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java @@ -51,6 +51,7 @@ import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind; import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; +import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove; import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate; import org.springframework.data.mongodb.core.ExecutableUpdateOperation.TerminatingUpdate; import org.springframework.data.mongodb.core.ExecutableUpdateOperation.UpdateWithQuery; @@ -104,6 +105,7 @@ class AbstractMongoQueryUnitTests { @Mock UpdateWithQuery updateWithQuery; @Mock UpdateWithUpdate updateWithUpdate; @Mock TerminatingUpdate terminatingUpdate; + @Mock ExecutableRemove executableRemove; @Mock BasicMongoPersistentEntity persitentEntityMock; @Mock MongoMappingContext mappingContextMock; @Mock DeleteResult deleteResultMock; @@ -130,8 +132,9 @@ void setUp() { doReturn(executableUpdate).when(mongoOperationsMock).update(any()); doReturn(updateWithQuery).when(executableUpdate).matching(any(Query.class)); doReturn(terminatingUpdate).when(updateWithQuery).apply(any(UpdateDefinition.class)); - - when(mongoOperationsMock.remove(any(), any(), anyString())).thenReturn(deleteResultMock); + doReturn(executableRemove).when(mongoOperationsMock).remove(any()); + doReturn(executableRemove).when(executableRemove).matching(any(Query.class)); + when(executableRemove.all()).thenReturn(deleteResultMock); when(mongoOperationsMock.updateMulti(any(), any(), any(), anyString())).thenReturn(updateResultMock); } @@ -140,8 +143,7 @@ void testDeleteExecutionCallsRemoveCorrectly() { createQueryForMethod("deletePersonByLastname", String.class).setDeleteQuery(true).execute(new Object[] { "booh" }); - verify(mongoOperationsMock, times(1)).remove(any(), eq(Person.class), eq("persons")); - verify(mongoOperationsMock, times(0)).find(any(), any(), any()); + verify(executableRemove, times(1)).all(); } @Test // DATAMONGO-566, DATAMONGO-1040 @@ -149,7 +151,7 @@ void testDeleteExecutionLoadsListOfRemovedDocumentsWhenReturnTypeIsCollectionLik createQueryForMethod("deleteByLastname", String.class).setDeleteQuery(true).execute(new Object[] { "booh" }); - verify(mongoOperationsMock, times(1)).findAllAndRemove(any(), eq(Person.class), eq("persons")); + verify(executableRemove, times(1)).findAndRemove(); } @Test // DATAMONGO-566 @@ -171,7 +173,7 @@ void testDeleteExecutionReturnsNrDocumentsDeletedFromWriteResult() { query.setDeleteQuery(true); assertThat(query.execute(new Object[] { "fake" })).isEqualTo(100L); - verify(mongoOperationsMock, times(1)).remove(any(), eq(Person.class), eq("persons")); + verify(executableRemove, times(1)).all(); } @Test // DATAMONGO-957 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java index 1c856394d8..f0ffebde20 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java @@ -22,8 +22,10 @@ import org.bson.Document; import org.junit.jupiter.api.Test; + import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; +import org.springframework.data.domain.Score; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; @@ -45,15 +47,15 @@ * @author Oliver Gierke * @author Christoph Strobl */ -public class MongoParametersParameterAccessorUnitTests { +class MongoParametersParameterAccessorUnitTests { - Distance DISTANCE = new Distance(2.5, Metrics.KILOMETERS); - RepositoryMetadata metadata = new DefaultRepositoryMetadata(PersonRepository.class); - MongoMappingContext context = new MongoMappingContext(); - ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); + private Distance DISTANCE = Distance.of(2.5, Metrics.KILOMETERS); + private RepositoryMetadata metadata = new DefaultRepositoryMetadata(PersonRepository.class); + private MongoMappingContext context = new MongoMappingContext(); + private ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); @Test - public void returnsUnboundedForDistanceIfNoneAvailable() throws NoSuchMethodException, SecurityException { + void returnsUnboundedForDistanceIfNoneAvailable() throws NoSuchMethodException, SecurityException { Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); @@ -64,7 +66,7 @@ public void returnsUnboundedForDistanceIfNoneAvailable() throws NoSuchMethodExce } @Test - public void returnsDistanceIfAvailable() throws NoSuchMethodException, SecurityException { + void returnsDistanceIfAvailable() throws NoSuchMethodException, SecurityException { Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); @@ -75,7 +77,7 @@ public void returnsDistanceIfAvailable() throws NoSuchMethodException, SecurityE } @Test // DATAMONGO-973 - public void shouldReturnAsFullTextStringWhenNoneDefinedForMethod() throws NoSuchMethodException, SecurityException { + void shouldReturnAsFullTextStringWhenNoneDefinedForMethod() throws NoSuchMethodException, SecurityException { Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); @@ -86,7 +88,7 @@ public void shouldReturnAsFullTextStringWhenNoneDefinedForMethod() throws NoSuch } @Test // DATAMONGO-973 - public void shouldProperlyConvertTextCriteria() throws NoSuchMethodException, SecurityException { + void shouldProperlyConvertTextCriteria() throws NoSuchMethodException, SecurityException { Method method = PersonRepository.class.getMethod("findByFirstname", String.class, TextCriteria.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); @@ -98,13 +100,13 @@ public void shouldProperlyConvertTextCriteria() throws NoSuchMethodException, Se } @Test // DATAMONGO-1110 - public void shouldDetectMinAndMaxDistance() throws NoSuchMethodException, SecurityException { + void shouldDetectMinAndMaxDistance() throws NoSuchMethodException, SecurityException { Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Range.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); - Distance min = new Distance(10, Metrics.KILOMETERS); - Distance max = new Distance(20, Metrics.KILOMETERS); + Distance min = Distance.of(10, Metrics.KILOMETERS); + Distance max = Distance.of(20, Metrics.KILOMETERS); MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod, new Object[] { new Point(10, 20), Distance.between(min, max) }); @@ -116,7 +118,7 @@ public void shouldDetectMinAndMaxDistance() throws NoSuchMethodException, Securi } @Test // DATAMONGO-1854 - public void shouldDetectCollation() throws NoSuchMethodException, SecurityException { + void shouldDetectCollation() throws NoSuchMethodException, SecurityException { Method method = PersonRepository.class.getMethod("findByFirstname", String.class, Collation.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); @@ -129,7 +131,7 @@ public void shouldDetectCollation() throws NoSuchMethodException, SecurityExcept } @Test // GH-2107 - public void shouldReturnUpdateIfPresent() throws NoSuchMethodException, SecurityException { + void shouldReturnUpdateIfPresent() throws NoSuchMethodException, SecurityException { Method method = PersonRepository.class.getMethod("findAndModifyByFirstname", String.class, UpdateDefinition.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); @@ -142,7 +144,7 @@ public void shouldReturnUpdateIfPresent() throws NoSuchMethodException, Security } @Test // GH-2107 - public void shouldReturnNullIfNoUpdatePresent() throws NoSuchMethodException, SecurityException { + void shouldReturnNullIfNoUpdatePresent() throws NoSuchMethodException, SecurityException { Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); @@ -153,6 +155,23 @@ public void shouldReturnNullIfNoUpdatePresent() throws NoSuchMethodException, Se assertThat(accessor.getUpdate()).isNull(); } + @Test // GH- + void shouldReturnRangeFromScore() throws NoSuchMethodException, SecurityException { + + Method method = PersonRepository.class.getMethod("findByFirstname", String.class, Score.class); + MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); + + MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod, + new Object[] { "foo", Score.of(1) }); + + Range scoreRange = accessor.getScoreRange(); + + assertThat(scoreRange).isNotNull(); + assertThat(scoreRange.getLowerBound().isBounded()).isFalse(); + assertThat(scoreRange.getUpperBound().isBounded()).isTrue(); + assertThat(scoreRange.getUpperBound().getValue()).contains(Score.of(1)); + } + interface PersonRepository extends Repository { List findByLocationNear(Point point); @@ -165,6 +184,8 @@ interface PersonRepository extends Repository { List findByFirstname(String firstname, Collation collation); + List findByFirstname(String firstname, Score score); + List findAndModifyByFirstname(String firstname, UpdateDefinition update); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java index 93674e23fc..fc1ffb971e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java @@ -27,6 +27,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.Vector; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Point; @@ -43,6 +45,7 @@ * * @author Oliver Gierke * @author Christoph Strobl + * @author Mark Paluch */ @ExtendWith(MockitoExtension.class) class MongoParametersUnitTests { @@ -184,6 +187,21 @@ void shouldReturnInvalidIndexIfUpdateDoesNotExist() throws NoSuchMethodException assertThat(parameters.getUpdateIndex()).isEqualTo(-1); } + @Test // GH-2107 + void shouldOmitVector() throws NoSuchMethodException, SecurityException { + + Method method = PersonRepository.class.getMethod("shouldOmitVector", Vector.class, Score.class, + Range.class, String.class); + MongoParameters parameters = new MongoParameters(ParametersSource.of(method), false); + + assertThat(parameters.getVectorIndex()).isEqualTo(0); + assertThat(parameters.getScoreIndex()).isEqualTo(1); + assertThat(parameters.getScoreRangeIndex()).isEqualTo(2); + + MongoParameters bindableParameters = parameters.getBindableParameters(); + assertThat(bindableParameters).hasSize(3); + } + interface PersonRepository { List findByLocationNear(Point point, Distance distance); @@ -205,5 +223,8 @@ interface PersonRepository { List findByText(String text, Collation collation); List findAndModifyByFirstname(String firstname, UpdateDefinition update, Pageable page); + + List shouldOmitVector(Vector vector, Score distance, Range range, + String country); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java index 609e0a0018..55e3df6b43 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java @@ -29,6 +29,7 @@ import org.bson.types.ObjectId; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; import org.springframework.data.geo.Distance; @@ -120,7 +121,7 @@ void createsIsNullQueryCorrectly() { void bindsMetricDistanceParameterToNearSphereCorrectly() throws Exception { Point point = new Point(10, 20); - Distance distance = new Distance(2.5, Metrics.KILOMETERS); + Distance distance = Distance.of(2.5, Metrics.KILOMETERS); Query query = query( where("location").nearSphere(point).maxDistance(distance.getNormalizedValue()).and("firstname").is("Dave")); @@ -131,7 +132,7 @@ void bindsMetricDistanceParameterToNearSphereCorrectly() throws Exception { void bindsDistanceParameterToNearCorrectly() throws Exception { Point point = new Point(10, 20); - Distance distance = new Distance(2.5); + Distance distance = Distance.of(2.5); Query query = query( where("location").near(point).maxDistance(distance.getNormalizedValue()).and("firstname").is("Dave")); @@ -405,7 +406,7 @@ void shouldCreateRegexWhenUsingNotContainsOnStringProperty() { void createsNonSphericalNearForDistanceWithDefaultMetric() { Point point = new Point(1.0, 1.0); - Distance distance = new Distance(1.0); + Distance distance = Distance.of(1.0); PartTree tree = new PartTree("findByLocationNear", Venue.class); MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, point, distance), context); @@ -445,7 +446,7 @@ void shouldCreateNearSphereQueryForSphericalProperty() { void shouldCreateNearSphereQueryForSphericalPropertyHavingDistanceWithDefaultMetric() { Point point = new Point(1.0, 1.0); - Distance distance = new Distance(1.0); + Distance distance = Distance.of(1.0); PartTree tree = new PartTree("findByAddress2dSphere_GeoNear", User.class); MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, point, distance), context); @@ -458,7 +459,7 @@ void shouldCreateNearSphereQueryForSphericalPropertyHavingDistanceWithDefaultMet void shouldCreateNearQueryForMinMaxDistance() { Point point = new Point(10, 20); - Range range = Distance.between(new Distance(10), new Distance(20)); + Range range = Distance.between(Distance.of(10), Distance.of(20)); PartTree tree = new PartTree("findByAddress_GeoNear", User.class); MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, point, range), context); @@ -664,7 +665,7 @@ void nearShouldUseMetricDistanceForGeoJsonTypes() { GeoJsonPoint point = new GeoJsonPoint(27.987901, 86.9165379); PartTree tree = new PartTree("findByLocationNear", User.class); MongoQueryCreator creator = new MongoQueryCreator(tree, - getAccessor(converter, point, new Distance(1, Metrics.KILOMETERS)), context); + getAccessor(converter, point, Distance.of(1, Metrics.KILOMETERS)), context); assertThat(creator.createQuery()).isEqualTo(query(where("location").nearSphere(point).maxDistance(1000.0D))); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java index 74ff20b148..2c0c996bc3 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java @@ -15,13 +15,17 @@ */ package org.springframework.data.mongodb.repository.query; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collections; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -41,6 +45,7 @@ import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind; import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFindNear; +import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.convert.DbRefResolver; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; @@ -56,8 +61,7 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.util.ReflectionUtils; import com.mongodb.client.result.DeleteResult; @@ -79,17 +83,16 @@ class MongoQueryExecutionUnitTests { @Mock FindWithQuery operationMock; @Mock TerminatingFind terminatingMock; @Mock TerminatingFindNear terminatingGeoMock; + @Mock ExecutableRemove removeMock; @Mock DbRefResolver dbRefResolver; - private SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); private Point POINT = new Point(10, 20); - private Distance DISTANCE = new Distance(2.5, Metrics.KILOMETERS); + private Distance DISTANCE = Distance.of(2.5, Metrics.KILOMETERS); private RepositoryMetadata metadata = new DefaultRepositoryMetadata(PersonRepository.class); private MongoMappingContext context = new MongoMappingContext(); private ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); private Method method = ReflectionUtils.findMethod(PersonRepository.class, "findByLocationNear", Point.class, - Distance.class, - Pageable.class); + Distance.class, Pageable.class); private MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); private MappingMongoConverter converter; @@ -152,8 +155,8 @@ void pagingGeoExecutionShouldUseCountFromResultWithOffsetAndResultsWithinPageSiz ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(converter, new MongoParametersParameterAccessor(queryMethod, new Object[] { POINT, DISTANCE, PageRequest.of(0, 10) })); - PartTreeMongoQuery query = new PartTreeMongoQuery(queryMethod, mongoOperationsMock, EXPRESSION_PARSER, - QueryMethodEvaluationContextProvider.DEFAULT); + PartTreeMongoQuery query = new PartTreeMongoQuery(queryMethod, mongoOperationsMock, + ValueExpressionDelegate.create()); PagingGeoNearExecution execution = new PagingGeoNearExecution(findOperationMock, queryMethod, accessor, query); execution.execute(new Query()); @@ -173,8 +176,8 @@ void pagingGeoExecutionRetrievesObjectsForPageableOutOfRange() { ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(converter, new MongoParametersParameterAccessor(queryMethod, new Object[] { POINT, DISTANCE, PageRequest.of(2, 10) })); - PartTreeMongoQuery query = new PartTreeMongoQuery(queryMethod, mongoOperationsMock, EXPRESSION_PARSER, - QueryMethodEvaluationContextProvider.DEFAULT); + PartTreeMongoQuery query = new PartTreeMongoQuery(queryMethod, mongoOperationsMock, + ValueExpressionDelegate.create()); PagingGeoNearExecution execution = new PagingGeoNearExecution(findOperationMock, queryMethod, accessor, query); execution.execute(new Query()); @@ -186,38 +189,36 @@ void pagingGeoExecutionRetrievesObjectsForPageableOutOfRange() { @Test // DATAMONGO-2351 void acknowledgedDeleteReturnsDeletedCount() { + doReturn(removeMock).when(removeMock).matching(any(Query.class)); + when(removeMock.all()).thenReturn(DeleteResult.acknowledged(10)); Method method = ReflectionUtils.findMethod(PersonRepository.class, "deleteAllByLastname", String.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); - when(mongoOperationsMock.remove(any(Query.class), any(Class.class), anyString())) - .thenReturn(DeleteResult.acknowledged(10)); - - assertThat(new DeleteExecution(mongoOperationsMock, queryMethod).execute(new Query())).isEqualTo(10L); + assertThat(new DeleteExecution(removeMock, queryMethod).execute(new Query())).isEqualTo(10L); } @Test // DATAMONGO-2351 void unacknowledgedDeleteReturnsZeroDeletedCount() { + doReturn(removeMock).when(removeMock).matching(any(Query.class)); + when(removeMock.all()).thenReturn(DeleteResult.unacknowledged()); Method method = ReflectionUtils.findMethod(PersonRepository.class, "deleteAllByLastname", String.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); - when(mongoOperationsMock.remove(any(Query.class), any(Class.class), anyString())) - .thenReturn(DeleteResult.unacknowledged()); - - assertThat(new DeleteExecution(mongoOperationsMock, queryMethod).execute(new Query())).isEqualTo(0L); + assertThat(new DeleteExecution(removeMock, queryMethod).execute(new Query())).isEqualTo(0L); } @Test // DATAMONGO-1997 void deleteExecutionWithEntityReturnTypeTriggersFindAndRemove() { - Method method = ReflectionUtils.findMethod(PersonRepository.class, "deleteByLastname", String.class); - MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); - Person person = new Person(); + doReturn(removeMock).when(removeMock).matching(any(Query.class)); + when(removeMock.findAndRemove()).thenReturn(List.of(person)); - when(mongoOperationsMock.findAndRemove(any(Query.class), any(Class.class), anyString())).thenReturn(person); + Method method = ReflectionUtils.findMethod(PersonRepository.class, "deleteByLastname", String.class); + MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); - assertThat(new DeleteExecution(mongoOperationsMock, queryMethod).execute(new Query())).isEqualTo(person); + assertThat(new DeleteExecution(removeMock, queryMethod).execute(new Query())).isEqualTo(person); } interface PersonRepository extends Repository { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java index 8f9824e14d..386d0fa4b5 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + import org.springframework.data.domain.Pageable; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoPage; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java index e0b9b77099..07c10592d9 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java @@ -45,8 +45,7 @@ import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.data.repository.query.ValueExpressionDelegate; /** * Unit tests for {@link PartTreeMongoQuery}. @@ -206,8 +205,7 @@ private PartTreeMongoQuery createQueryForMethod(String methodName, Class... p MongoQueryMethod queryMethod = new MongoQueryMethod(method, new DefaultRepositoryMetadata(Repo.class), factory, mappingContext); - return new PartTreeMongoQuery(queryMethod, mongoOperationsMock, new SpelExpressionParser(), - QueryMethodEvaluationContextProvider.DEFAULT); + return new PartTreeMongoQuery(queryMethod, mongoOperationsMock, ValueExpressionDelegate.create()); } catch (Exception e) { throw new IllegalArgumentException(e.getMessage(), e); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecutionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecutionUnitTests.java index 21d5dc71fb..1fbd60414a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecutionUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecutionUnitTests.java @@ -46,7 +46,7 @@ import org.springframework.data.mongodb.repository.Person; import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.DeleteExecution; import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.GeoNearExecution; -import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; import org.springframework.util.ClassUtils; import com.mongodb.client.result.DeleteResult; @@ -71,10 +71,10 @@ public void geoNearExecutionShouldApplyQuerySettings() throws Exception { Query query = new Query(); when(parameterAccessor.getGeoNearLocation()).thenReturn(new Point(1, 2)); when(parameterAccessor.getDistanceRange()) - .thenReturn(Range.from(Bound.inclusive(new Distance(10))).to(Bound.inclusive(new Distance(15)))); + .thenReturn(Range.from(Bound.inclusive(Distance.of(10))).to(Bound.inclusive(Distance.of(15)))); when(parameterAccessor.getPageable()).thenReturn(PageRequest.of(1, 10)); - new GeoNearExecution(operations, parameterAccessor, ClassTypeInformation.fromReturnTypeOf(geoNear)).execute(query, + new GeoNearExecution(operations, parameterAccessor, TypeInformation.fromReturnTypeOf(geoNear)).execute(query, Person.class, "person"); ArgumentCaptor queryArgumentCaptor = ArgumentCaptor.forClass(NearQuery.class); @@ -83,8 +83,8 @@ public void geoNearExecutionShouldApplyQuerySettings() throws Exception { NearQuery nearQuery = queryArgumentCaptor.getValue(); assertThat(nearQuery.toDocument().get("near")).isEqualTo(Arrays.asList(1d, 2d)); assertThat(nearQuery.getSkip()).isEqualTo(10L); - assertThat(nearQuery.getMinDistance()).isEqualTo(new Distance(10)); - assertThat(nearQuery.getMaxDistance()).isEqualTo(new Distance(15)); + assertThat(nearQuery.getMinDistance()).isEqualTo(Distance.of(10)); + assertThat(nearQuery.getMaxDistance()).isEqualTo(Distance.of(15)); } @Test // DATAMONGO-1444 @@ -96,7 +96,7 @@ public void geoNearExecutionShouldApplyMinimalSettings() throws Exception { when(parameterAccessor.getGeoNearLocation()).thenReturn(new Point(1, 2)); when(parameterAccessor.getDistanceRange()).thenReturn(Range.unbounded()); - new GeoNearExecution(operations, parameterAccessor, ClassTypeInformation.fromReturnTypeOf(geoNear)).execute(query, + new GeoNearExecution(operations, parameterAccessor, TypeInformation.fromReturnTypeOf(geoNear)).execute(query, Person.class, "person"); ArgumentCaptor queryArgumentCaptor = ArgumentCaptor.forClass(NearQuery.class); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java index 82cd0a157c..14cbbc0394 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java @@ -17,7 +17,6 @@ import static org.assertj.core.api.Assertions.*; -import org.springframework.data.mongodb.repository.query.MongoQueryMethodUnitTests.PersonRepository; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,6 +26,7 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java index c6047ce30d..b55ee77732 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java @@ -27,6 +27,7 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -34,6 +35,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.reactivestreams.Publisher; + import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.ReactiveMongoOperations; @@ -54,10 +56,8 @@ import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.reactive.ReactiveCrudRepository; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import com.mongodb.ReadPreference; @@ -71,8 +71,6 @@ @ExtendWith(MockitoExtension.class) public class ReactiveStringBasedAggregationUnitTests { - SpelExpressionParser PARSER = new SpelExpressionParser(); - @Mock ReactiveMongoOperations operations; @Mock DbRefResolver dbRefResolver; MongoConverter converter; @@ -226,8 +224,7 @@ private ReactiveStringBasedAggregation createAggregationForMethod(String name, C ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); ReactiveMongoQueryMethod queryMethod = new ReactiveMongoQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class), factory, converter.getMappingContext()); - return new ReactiveStringBasedAggregation(queryMethod, operations, PARSER, - ReactiveQueryMethodEvaluationContextProvider.DEFAULT); + return new ReactiveStringBasedAggregation(queryMethod, operations, ValueExpressionDelegate.create()); } private List pipelineOf(AggregationInvocation invocation) { @@ -242,27 +239,24 @@ private Class inputTypeOf(AggregationInvocation invocation) { return invocation.aggregation.getInputType(); } - @Nullable - private Collation collationOf(AggregationInvocation invocation) { + private @Nullable Collation collationOf(AggregationInvocation invocation) { return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getCollation().orElse(null) : null; } - @Nullable - private Object hintOf(AggregationInvocation invocation) { - return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getHintObject().orElse(null) + private @Nullable Object hintOf(AggregationInvocation invocation) { + return invocation.aggregation.getOptions() != null + ? invocation.aggregation.getOptions().getHintObject().orElse(null) : null; } private Boolean skipResultsOf(AggregationInvocation invocation) { - return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().isSkipResults() - : false; + return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().isSkipResults() : false; } @Nullable private ReadPreference readPreferenceOf(AggregationInvocation invocation) { - return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getReadPreference() - : null; + return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getReadPreference() : null; } private Class targetTypeOf(AggregationInvocation invocation) { @@ -284,7 +278,7 @@ private interface SampleRepository extends ReactiveCrudRepository @Aggregation(GROUP_BY_LASTNAME_STRING_WITH_SPEL_PARAMETER_PLACEHOLDER) Mono spelParameterReplacementAggregation(String arg0); - @Aggregation(pipeline = {RAW_GROUP_BY_LASTNAME_STRING, GROUP_BY_LASTNAME_STRING_WITH_SPEL_PARAMETER_PLACEHOLDER}) + @Aggregation(pipeline = { RAW_GROUP_BY_LASTNAME_STRING, GROUP_BY_LASTNAME_STRING_WITH_SPEL_PARAMETER_PLACEHOLDER }) Mono multiOperationPipeline(String arg0); @Aggregation(pipeline = RAW_GROUP_BY_LASTNAME_STRING, collation = "de_AT") diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java index 72f9626a57..7358bf4ce6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java @@ -56,8 +56,6 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; -import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.spel.spi.EvaluationContextExtension; import org.springframework.data.spel.spi.ReactiveEvaluationContextExtension; @@ -248,8 +246,8 @@ public void shouldSupportNonQuotedBinaryDataReplacement() throws Exception { ReactiveStringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameAsBinary", byte[].class); org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor).block(); - org.springframework.data.mongodb.core.query.Query reference = new BasicQuery( - "{'lastname' : { '$binary' : '" + Base64.getEncoder().encodeToString(binaryData) + "', '$type' : '" + 0 + "'}}"); + org.springframework.data.mongodb.core.query.Query reference = new BasicQuery("{'lastname' : { '$binary' : '" + + Base64.getEncoder().encodeToString(binaryData) + "', '$type' : '" + 0 + "'}}"); assertThat(query.getQueryObject().toJson()).isEqualTo(reference.getQueryObject().toJson()); } @@ -266,16 +264,14 @@ void shouldConsiderReactiveSpelExtension() throws Exception { assertThat(query.getQueryObject().toJson()).isEqualTo(reference.getQueryObject().toJson()); } - private ReactiveStringBasedMongoQuery createQueryForMethod( - String name, Class... parameters) - throws Exception { + private ReactiveStringBasedMongoQuery createQueryForMethod(String name, Class... parameters) throws Exception { Method method = SampleRepository.class.getMethod(name, parameters); ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); ReactiveMongoQueryMethod queryMethod = new ReactiveMongoQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class), factory, converter.getMappingContext()); - QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor( - environment, Collections.singletonList(ReactiveSpelExtension.INSTANCE)); + QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor(environment, + Collections.singletonList(ReactiveSpelExtension.INSTANCE)); return new ReactiveStringBasedMongoQuery(queryMethod, operations, new ValueExpressionDelegate(accessor, PARSER)); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java index 85a8650b26..827168007e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java @@ -27,6 +27,7 @@ import java.util.stream.Stream; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -35,6 +36,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; + import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -62,9 +64,7 @@ import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.util.ClassUtils; import com.mongodb.MongoClientSettings; @@ -81,8 +81,6 @@ @MockitoSettings(strictness = Strictness.LENIENT) public class StringBasedAggregationUnitTests { - private SpelExpressionParser PARSER = new SpelExpressionParser(); - @Mock MongoOperations operations; @Mock DbRefResolver dbRefResolver; @Mock AggregationResults aggregationResults; @@ -254,8 +252,7 @@ void aggregateRaisesErrorOnInvalidReturnType() { factory, converter.getMappingContext()); assertThatExceptionOfType(InvalidMongoDbApiUsageException.class) // - .isThrownBy(() -> new StringBasedAggregation(queryMethod, operations, PARSER, - QueryMethodEvaluationContextProvider.DEFAULT)) // + .isThrownBy(() -> new StringBasedAggregation(queryMethod, operations, ValueExpressionDelegate.create())) // .withMessageContaining("pageIsUnsupported") // .withMessageContaining("Page"); } @@ -311,7 +308,7 @@ private StringBasedAggregation createAggregationForMethod(String name, Class. ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); MongoQueryMethod queryMethod = new MongoQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class), factory, converter.getMappingContext()); - return new StringBasedAggregation(queryMethod, operations, PARSER, QueryMethodEvaluationContextProvider.DEFAULT); + return new StringBasedAggregation(queryMethod, operations, ValueExpressionDelegate.create()); } private List pipelineOf(AggregationInvocation invocation) { @@ -326,27 +323,24 @@ private Class inputTypeOf(AggregationInvocation invocation) { return invocation.aggregation.getInputType(); } - @Nullable - private Collation collationOf(AggregationInvocation invocation) { + private @Nullable Collation collationOf(AggregationInvocation invocation) { return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getCollation().orElse(null) : null; } - @Nullable - private Object hintOf(AggregationInvocation invocation) { - return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getHintObject().orElse(null) + private @Nullable Object hintOf(AggregationInvocation invocation) { + return invocation.aggregation.getOptions() != null + ? invocation.aggregation.getOptions().getHintObject().orElse(null) : null; } private Boolean skipResultsOf(AggregationInvocation invocation) { - return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().isSkipResults() - : false; + return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().isSkipResults() : false; } @Nullable private ReadPreference readPreferenceOf(AggregationInvocation invocation) { - return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getReadPreference() - : null; + return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getReadPreference() : null; } private Class targetTypeOf(AggregationInvocation invocation) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java index 1927378e80..91f23bb049 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java @@ -18,11 +18,15 @@ import java.util.Arrays; import java.util.Iterator; +import org.jspecify.annotations.Nullable; + import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; +import org.springframework.data.domain.Score; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Vector; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.convert.MongoWriter; @@ -30,7 +34,6 @@ import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.repository.query.ParameterAccessor; -import org.springframework.lang.Nullable; /** * Simple {@link ParameterAccessor} that returns the given parameters unfiltered. @@ -73,6 +76,21 @@ public StubParameterAccessor(Object... values) { } } + @Override + public Vector getVector() { + return null; + } + + @Override + public @org.jspecify.annotations.Nullable Score getScore() { + return null; + } + + @Override + public @org.jspecify.annotations.Nullable Range getScoreRange() { + return null; + } + @Override public ScrollPosition getScrollPosition() { return null; @@ -121,7 +139,7 @@ public Collation getCollation() { * @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getValues() */ @Override - public Object[] getValues() { + public Object @Nullable[] getValues() { return this.values; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregationUnitTests.java new file mode 100644 index 0000000000..819bba5a48 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregationUnitTests.java @@ -0,0 +1,109 @@ +/* + * 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.mongodb.repository.query; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Method; + +import org.bson.conversions.Bson; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.SearchResults; +import org.springframework.data.domain.Vector; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.repository.VectorSearch; +import org.springframework.data.mongodb.repository.query.VectorSearchDelegate.QueryContainer; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.query.ValueExpressionDelegate; + +/** + * Unit tests for {@link VectorSearchAggregation}. + * + * @author Mark Paluch + */ +class VectorSearchAggregationUnitTests { + + MongoOperations operationsMock; + MongoMappingContext context; + MappingMongoConverter converter; + + @BeforeEach + public void setUp() { + + context = new MongoMappingContext(); + converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, context); + operationsMock = Mockito.mock(MongoOperations.class); + + when(operationsMock.getConverter()).thenReturn(converter); + when(operationsMock.execute(any())).thenReturn(Bson.DEFAULT_CODEC_REGISTRY); + } + + @Test + void derivesPrefilter() throws Exception { + + VectorSearchAggregation aggregation = aggregation(SampleRepository.class, "searchByCountryAndEmbeddingNear", + String.class, Vector.class, Score.class, Limit.class); + + QueryContainer query = aggregation.createVectorSearchQuery( + aggregation.getQueryMethod().getResultProcessor(), + new MongoParametersParameterAccessor(aggregation.getQueryMethod(), + new Object[] { "de", Vector.of(1f), Score.of(1), Limit.unlimited() }), + Object.class); + + assertThat(query.query().getQueryObject()).containsEntry("country", "de"); + } + + private VectorSearchAggregation aggregation(Class repository, String name, Class... parameters) + throws Exception { + + Method method = repository.getMethod(name, parameters); + ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); + MongoQueryMethod queryMethod = new MongoQueryMethod(method, new DefaultRepositoryMetadata(repository), factory, + context); + return new VectorSearchAggregation(queryMethod, operationsMock, ValueExpressionDelegate.create()); + } + + interface SampleRepository extends CrudRepository { + + @VectorSearch(indexName = "cos-index") + SearchResults searchByCountryAndEmbeddingNear(String country, Vector vector, Score similarity, + Limit limit); + + } + + static class WithVectorFields { + + String id; + String country; + String description; + + Vector embedding; + + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegateUnitTests.java new file mode 100644 index 0000000000..078c01eece --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegateUnitTests.java @@ -0,0 +1,254 @@ +/* + * 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.mongodb.repository.query; + +import static org.mockito.Mockito.mock; +import static org.springframework.data.mongodb.test.util.Assertions.assertThat; + +import java.lang.reflect.Method; +import java.util.List; + +import org.bson.Document; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.SearchResults; +import org.springframework.data.domain.Vector; +import org.springframework.data.mapping.model.ValueExpressionEvaluator; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationPipeline; +import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.repository.VectorSearch; +import org.springframework.data.mongodb.repository.query.VectorSearchDelegate.QueryContainer; +import org.springframework.data.mongodb.util.aggregation.TestAggregationContext; +import org.springframework.data.mongodb.util.json.ParameterBindingContext; +import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.AnnotationRepositoryMetadata; +import org.springframework.data.repository.query.ValueExpressionDelegate; + +/** + * Unit tests for {@link VectorSearchDelegate}. + * + * @author Mark Paluch + * @author Christoph Strobl + */ +class VectorSearchDelegateUnitTests { + + MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, new MongoMappingContext()); + + @Test + void shouldConsiderDerivedLimit() throws ReflectiveOperationException { + + Method method = VectorSearchRepository.class.getMethod("searchTop10ByEmbeddingNear", Vector.class, Score.class); + + MongoQueryMethod queryMethod = getMongoQueryMethod(method); + MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1)); + + QueryContainer container = createQueryContainer(queryMethod, accessor); + + assertThat(container.query().getLimit()).isEqualTo(10); + assertThat(numCandidates(container.pipeline())).isEqualTo(10 * 20); + } + + @Test + void shouldNotSetNumCandidates() throws ReflectiveOperationException { + + Method method = VectorSearchRepository.class.getMethod("searchTop10EnnByEmbeddingNear", Vector.class, Score.class); + + MongoQueryMethod queryMethod = getMongoQueryMethod(method); + MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1)); + + QueryContainer container = createQueryContainer(queryMethod, accessor); + + assertThat(container.query().getLimit()).isEqualTo(10); + assertThat(numCandidates(container.pipeline())).isNull(); + } + + @Test + void shouldConsiderProvidedLimit() throws ReflectiveOperationException { + + Method method = VectorSearchRepository.class.getMethod("searchTop10ByEmbeddingNear", Vector.class, Score.class, + Limit.class); + + MongoQueryMethod queryMethod = getMongoQueryMethod(method); + MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1), Limit.of(11)); + + QueryContainer container = createQueryContainer(queryMethod, accessor); + + assertThat(container.query().getLimit()).isEqualTo(11); + assertThat(numCandidates(container.pipeline())).isEqualTo(11 * 20); + } + + @Test + void considersDerivedQueryPart() throws ReflectiveOperationException { + + Method method = VectorSearchRepository.class.getMethod("searchTop10ByFirstNameAndEmbeddingNear", String.class, + Vector.class, Score.class); + + MongoQueryMethod queryMethod = getMongoQueryMethod(method); + MongoParametersParameterAccessor accessor = getAccessor(queryMethod, "spring", Vector.of(1, 2), Score.of(1)); + + QueryContainer container = createQueryContainer(queryMethod, accessor); + + assertThat(vectorSearchStageOf(container.pipeline())).containsEntry("$vectorSearch.filter", + new Document("first_name", "spring")); + } + + @Test + void considersDerivedQueryPartInDifferentOrder() throws ReflectiveOperationException { + + Method method = VectorSearchRepository.class.getMethod("searchTop10ByEmbeddingNearAndFirstName", Vector.class, + Score.class, String.class); + + MongoQueryMethod queryMethod = getMongoQueryMethod(method); + MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1), "spring"); + + QueryContainer container = createQueryContainer(queryMethod, accessor); + + assertThat(vectorSearchStageOf(container.pipeline())).containsEntry("$vectorSearch.filter", + new Document("first_name", "spring")); + } + + @Test + void defaultSortsByScore() throws NoSuchMethodException { + + Method method = VectorSearchRepository.class.getMethod("searchTop10ByEmbeddingNear", Vector.class, Score.class, + Limit.class); + + MongoQueryMethod queryMethod = getMongoQueryMethod(method); + MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1), Limit.of(10)); + + QueryContainer container = createQueryContainer(queryMethod, accessor); + + List stages = container.pipeline().lastOperation() + .toPipelineStages(TestAggregationContext.contextFor(WithVector.class)); + + assertThat(stages).containsExactly(new Document("$sort", new Document("__score__", -1))); + } + + @Test + void usesDerivedSort() throws NoSuchMethodException { + + Method method = VectorSearchRepository.class.getMethod("searchByEmbeddingNearOrderByFirstName", Vector.class, + Score.class, Limit.class); + + MongoQueryMethod queryMethod = getMongoQueryMethod(method); + MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1), Limit.of(11)); + + QueryContainer container = createQueryContainer(queryMethod, accessor); + AggregationPipeline aggregationPipeline = container.pipeline(); + + List stages = aggregationPipeline.lastOperation() + .toPipelineStages(TestAggregationContext.contextFor(WithVector.class)); + + assertThat(stages).containsExactly(new Document("$sort", new Document("first_name", 1).append("__score__", -1))); + } + + Document vectorSearchStageOf(AggregationPipeline pipeline) { + return pipeline.firstOperation().toPipelineStages(TestAggregationContext.contextFor(WithVector.class)).get(0); + } + + private QueryContainer createQueryContainer(MongoQueryMethod queryMethod, MongoParametersParameterAccessor accessor) { + + VectorSearchDelegate delegate = new VectorSearchDelegate(queryMethod, converter, ValueExpressionDelegate.create()); + + return delegate.createQuery(mock(ValueExpressionEvaluator.class), queryMethod.getResultProcessor(), accessor, null, + new ParameterBindingDocumentCodec(), mock(ParameterBindingContext.class)); + } + + private MongoQueryMethod getMongoQueryMethod(Method method) { + RepositoryMetadata metadata = AnnotationRepositoryMetadata.getMetadata(method.getDeclaringClass()); + return new MongoQueryMethod(method, metadata, new SpelAwareProxyProjectionFactory(), converter.getMappingContext()); + } + + private static MongoParametersParameterAccessor getAccessor(MongoQueryMethod queryMethod, Object... values) { + return new MongoParametersParameterAccessor(queryMethod, values); + } + + @Nullable + private static Integer numCandidates(AggregationPipeline pipeline) { + + Document $vectorSearch = pipeline.firstOperation().toPipelineStages(Aggregation.DEFAULT_CONTEXT).get(0); + if ($vectorSearch.containsKey("$vectorSearch")) { + Object value = $vectorSearch.get("$vectorSearch", Document.class).get("numCandidates"); + return value instanceof Number i ? i.intValue() : null; + } + return null; + } + + interface VectorSearchRepository extends Repository { + + @VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ANN) + SearchResults searchTop10ByEmbeddingNear(Vector vector, Score similarity); + + @VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ANN) + SearchResults searchTop10ByFirstNameAndEmbeddingNear(String firstName, Vector vector, Score similarity); + + @VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ANN) + SearchResults searchTop10ByEmbeddingNearAndFirstName(Vector vector, Score similarity, String firstname); + + @VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ENN) + SearchResults searchTop10EnnByEmbeddingNear(Vector vector, Score similarity); + + @VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ANN) + SearchResults searchTop10ByEmbeddingNear(Vector vector, Score similarity, Limit limit); + + @VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ANN) + SearchResults searchByEmbeddingNearOrderByFirstName(Vector vector, Score similarity, Limit limit); + + } + + static class WithVector { + + Vector embedding; + + String lastName; + + @Field("first_name") String firstName; + + public Vector getEmbedding() { + return embedding; + } + + public void setEmbedding(Vector embedding) { + this.embedding = embedding; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java index c40f24dacb..3d0e468155 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java @@ -31,9 +31,7 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; @@ -41,9 +39,8 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.repository.Person; import org.springframework.data.mongodb.repository.ReadPreference; -import org.springframework.data.mongodb.repository.query.MongoEntityInformation; import org.springframework.data.repository.ListCrudRepository; -import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.EntityInformation; /** * Unit test for {@link MongoRepositoryFactory}. @@ -54,27 +51,27 @@ */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -public class MongoRepositoryFactoryUnitTests { +class MongoRepositoryFactoryUnitTests { @Mock MongoOperations template; - MongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, new MongoMappingContext()); + private MongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, new MongoMappingContext()); @BeforeEach - public void setUp() { + void setUp() { when(template.getConverter()).thenReturn(converter); } @Test - public void usesMappingMongoEntityInformationIfMappingContextSet() { + void usesMappingMongoEntityInformationIfMappingContextSet() { MongoRepositoryFactory factory = new MongoRepositoryFactory(template); - MongoEntityInformation entityInformation = factory.getEntityInformation(Person.class); + EntityInformation entityInformation = factory.getEntityInformation(Person.class); assertThat(entityInformation instanceof MappingMongoEntityInformation).isTrue(); } @Test // DATAMONGO-385 - public void createsRepositoryWithIdTypeLong() { + void createsRepositoryWithIdTypeLong() { MongoRepositoryFactory factory = new MongoRepositoryFactory(template); MyPersonRepository repository = factory.getRepository(MyPersonRepository.class); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributorUnitTests.java new file mode 100644 index 0000000000..564115fed0 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributorUnitTests.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.mongodb.repository.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Iterator; + +import org.junit.jupiter.api.Test; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.repository.User; +import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; + +/** + * Unit tests for {@link MongoRepositoryFragmentsContributor}. + * + * @author Mark Paluch + */ +class MongoRepositoryFragmentsContributorUnitTests { + + @Test // GH-4964 + void composedContributorShouldCreateFragments() { + + MongoMappingContext mappingContext = new MongoMappingContext(); + MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext); + MongoOperations operations = mock(MongoOperations.class); + when(operations.getConverter()).thenReturn(converter); + + MongoRepositoryFragmentsContributor contributor = MongoRepositoryFragmentsContributor.DEFAULT + .andThen(MyMongoRepositoryFragmentsContributor.INSTANCE); + + RepositoryComposition.RepositoryFragments fragments = contributor.contribute( + AbstractRepositoryMetadata.getMetadata(QuerydslUserRepository.class), + new MappingMongoEntityInformation<>(mappingContext.getPersistentEntity(User.class)), operations); + + assertThat(fragments).hasSize(2); + + Iterator> iterator = fragments.iterator(); + + RepositoryFragment querydsl = iterator.next(); + assertThat(querydsl.getImplementationClass()).contains(QuerydslMongoPredicateExecutor.class); + + RepositoryFragment additional = iterator.next(); + assertThat(additional.getImplementationClass()).contains(MyFragment.class); + } + + enum MyMongoRepositoryFragmentsContributor implements MongoRepositoryFragmentsContributor { + + INSTANCE; + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + MongoEntityInformation entityInformation, MongoOperations operations) { + return RepositoryComposition.RepositoryFragments.just(new MyFragment()); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + return RepositoryComposition.RepositoryFragments.just(new MyFragment()); + } + } + + static class MyFragment { + + } + + interface QuerydslUserRepository extends Repository, QuerydslPredicateExecutor {} + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java index 7d9024e2fb..5f0800aba6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java @@ -75,7 +75,8 @@ public class QuerydslMongoPredicateExecutorIntegrationTests { public void setup() { MongoRepositoryFactory factory = new MongoRepositoryFactory(operations); - MongoEntityInformation entityInformation = factory.getEntityInformation(Person.class); + MongoEntityInformation entityInformation = factory + .getEntityInformation(Person.class); repository = new QuerydslMongoPredicateExecutor<>(entityInformation, operations); operations.dropCollection(Person.class); @@ -246,7 +247,8 @@ protected MongoDatabase doGetDatabase() { }; MongoRepositoryFactory factory = new MongoRepositoryFactory(ops); - MongoEntityInformation entityInformation = factory.getEntityInformation(Person.class); + MongoEntityInformation entityInformation = factory + .getEntityInformation(Person.class); repository = new QuerydslMongoPredicateExecutor<>(entityInformation, ops); repository.findOne(person.firstname.contains("batman")); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributorUnitTests.java new file mode 100644 index 0000000000..065ff27654 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributorUnitTests.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.mongodb.repository.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Iterator; + +import org.junit.jupiter.api.Test; +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.repository.User; +import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; + +/** + * Unit tests for {@link ReactiveMongoRepositoryFragmentsContributor}. + * + * @author Mark Paluch + */ +class ReactiveMongoRepositoryFragmentsContributorUnitTests { + + @Test // GH-4964 + void composedContributorShouldCreateFragments() { + + MongoMappingContext mappingContext = new MongoMappingContext(); + MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext); + ReactiveMongoOperations operations = mock(ReactiveMongoOperations.class); + when(operations.getConverter()).thenReturn(converter); + + ReactiveMongoRepositoryFragmentsContributor contributor = ReactiveMongoRepositoryFragmentsContributor.DEFAULT + .andThen(MyMongoRepositoryFragmentsContributor.INSTANCE); + + RepositoryComposition.RepositoryFragments fragments = contributor.contribute( + AbstractRepositoryMetadata.getMetadata(QuerydslUserRepository.class), + new MappingMongoEntityInformation<>(mappingContext.getPersistentEntity(User.class)), operations); + + assertThat(fragments).hasSize(2); + + Iterator> iterator = fragments.iterator(); + + RepositoryFragment querydsl = iterator.next(); + assertThat(querydsl.getImplementationClass()).contains(ReactiveQuerydslMongoPredicateExecutor.class); + + RepositoryFragment additional = iterator.next(); + assertThat(additional.getImplementationClass()).contains(MyFragment.class); + } + + enum MyMongoRepositoryFragmentsContributor implements ReactiveMongoRepositoryFragmentsContributor { + + INSTANCE; + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + MongoEntityInformation entityInformation, ReactiveMongoOperations operations) { + return RepositoryComposition.RepositoryFragments.just(new MyFragment()); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + return RepositoryComposition.RepositoryFragments.just(new MyFragment()); + } + } + + static class MyFragment { + + } + + interface QuerydslUserRepository extends Repository, ReactiveQuerydslPredicateExecutor {} + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java index 807b7aec22..c807a1bcbd 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java @@ -111,7 +111,8 @@ public static void cleanDb() { public void setup() { ReactiveMongoRepositoryFactory factory = new ReactiveMongoRepositoryFactory(operations); - MongoEntityInformation entityInformation = factory.getEntityInformation(Person.class); + MongoEntityInformation entityInformation = factory + .getEntityInformation(Person.class); repository = new ReactiveQuerydslMongoPredicateExecutor<>(entityInformation, operations); dave = new Person("Dave", "Matthews", 42); @@ -326,7 +327,8 @@ protected Mono doGetDatabase() { }; ReactiveMongoRepositoryFactory factory = new ReactiveMongoRepositoryFactory(ops); - MongoEntityInformation entityInformation = factory.getEntityInformation(Person.class); + MongoEntityInformation entityInformation = factory + .getEntityInformation(Person.class); repository = new ReactiveQuerydslMongoPredicateExecutor<>(entityInformation, ops); repository.findOne(person.firstname.contains("batman")) // diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java index f2fd993ef8..17a045b7a1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java @@ -32,8 +32,8 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.springframework.data.mongodb.test.util.CleanMongoDB.Struct; -import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; +import com.mongodb.client.ListCollectionNamesIterable; import com.mongodb.client.ListDatabasesIterable; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoCollection; @@ -74,11 +74,11 @@ void setUp() throws ClassNotFoundException { when(mongoClientMock.getDatabase(eq("db2"))).thenReturn(db2mock); // collections have to exist - MongoIterable collectionIterable = mock(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db1mock).collectionNameIterableType()); + MongoIterable collectionIterable = mock(ListCollectionNamesIterable.class); when(collectionIterable.into(any(Collection.class))).thenReturn(Arrays.asList("db1collection1", "db1collection2")); doReturn(collectionIterable).when(db1mock).listCollectionNames(); - MongoIterable collectionIterable2 = mock(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db2mock).collectionNameIterableType()); + MongoIterable collectionIterable2 = mock(ListCollectionNamesIterable.class); when(collectionIterable2.into(any(Collection.class))).thenReturn(Collections.singletonList("db2collection1")); doReturn(collectionIterable2).when(db2mock).listCollectionNames(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MappingContextConfigurer.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MappingContextConfigurer.java index 15a0538600..eda1e501a0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MappingContextConfigurer.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MappingContextConfigurer.java @@ -20,7 +20,7 @@ import java.util.HashSet; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Utility to configure {@link org.springframework.data.mongodb.core.mapping.MongoMappingContext} properties. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java index 40948a0e22..771c17c4a9 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java @@ -23,12 +23,12 @@ import java.util.function.Supplier; import java.util.stream.Collectors; +import com.mongodb.client.MongoClients; import org.bson.Document; import org.springframework.context.ApplicationContext; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; import org.testcontainers.shaded.org.awaitility.Awaitility; import com.mongodb.MongoWriteException; @@ -45,6 +45,14 @@ public class MongoTestTemplate extends MongoTemplate { private final MongoTestTemplateConfiguration cfg; + public MongoTestTemplate() { + this("test"); + } + + public MongoTestTemplate(String databaseName) { + this(MongoClients.create(), databaseName); + } + public MongoTestTemplate(MongoClient client, String database, Class... initialEntities) { this(cfg -> { cfg.configureDatabaseFactory(it -> { @@ -96,7 +104,7 @@ public void flush() { } public void flushDatabase() { - flush(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(getDb()).listCollectionNames()); + flush(getDb().listCollectionNames()); } public void flush(Iterable collections) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplateConfiguration.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplateConfiguration.java index 09149c02ef..8300690ccd 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplateConfiguration.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplateConfiguration.java @@ -20,6 +20,7 @@ import java.util.function.Consumer; import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectFactory; import org.springframework.context.ApplicationContext; @@ -39,7 +40,6 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.event.AuditingEntityCallback; import org.springframework.data.mongodb.core.mapping.event.MongoMappingEvent; -import org.springframework.lang.Nullable; /** * @author Christoph Strobl diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapterUnitTests.java deleted file mode 100644 index ab8e17a469..0000000000 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapterUnitTests.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.mongodb.util; - -import static org.assertj.core.api.Assertions.*; - -import org.junit.jupiter.api.Test; -import org.springframework.data.mongodb.test.util.ExcludeReactiveClientFromClassPath; -import org.springframework.data.mongodb.test.util.ExcludeSyncClientFromClassPath; -import org.springframework.util.ClassUtils; - -/** - * @author Christoph Strobl - */ -class MongoCompatibilityAdapterUnitTests { - - @Test // GH-4578 - @ExcludeReactiveClientFromClassPath - void returnsListCollectionNameIterableTypeCorrectly() { - - String expectedType = MongoClientVersion.isVersion5orNewer() ? "ListCollectionNamesIterable" : "MongoIterable"; - assertThat(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(null).collectionNameIterableType()) - .satisfies(type -> assertThat(ClassUtils.getShortName(type)).isEqualTo(expectedType)); - - } - - @Test // GH-4578 - @ExcludeSyncClientFromClassPath - void returnsListCollectionNamePublisherTypeCorrectly() { - - String expectedType = MongoClientVersion.isVersion5orNewer() ? "ListCollectionNamesPublisher" : "Publisher"; - assertThat(MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(null).collectionNamePublisherType()) - .satisfies(type -> assertThat(ClassUtils.getShortName(type)).isEqualTo(expectedType)); - - } -} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/SpringJsonWriterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/SpringJsonWriterUnitTests.java new file mode 100644 index 0000000000..878623944c --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/SpringJsonWriterUnitTests.java @@ -0,0 +1,161 @@ +/* + * 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.mongodb.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.bson.BsonRegularExpression; +import org.bson.BsonTimestamp; +import org.bson.types.Decimal128; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link SpringJsonWriter}. + * + * @author Christoph Strobl + * @since 5.0 + */ +class SpringJsonWriterUnitTests { + + StringBuffer buffer; + SpringJsonWriter writer; + + @BeforeEach + void beforeEach() { + buffer = new StringBuffer(); + writer = new SpringJsonWriter(buffer); + } + + @Test + void writeDocumentWithSingleEntry() { + + writer.writeStartDocument(); + writer.writeString("key", "value"); + writer.writeEndDocument(); + + assertThat(buffer).isEqualToIgnoringWhitespace("{'key':'value'}"); + } + + @Test + void writeDocumentWithMultipleEntries() { + + writer.writeStartDocument(); + writer.writeString("key-1", "v1"); + writer.writeString("key-2", "v2"); + writer.writeEndDocument(); + + assertThat(buffer).isEqualToIgnoringWhitespace("{'key-1':'v1','key-2':'v2'}"); + } + + @Test + void writeInt32() { + + writer.writeInt32("int32", 32); + + assertThat(buffer).isEqualToIgnoringWhitespace("'int32':{'$numberInt':'32'}"); + } + + @Test + void writeInt64() { + + writer.writeInt64("int64", 64); + + assertThat(buffer).isEqualToIgnoringWhitespace("'int64':{'$numberLong':'64'}"); + } + + @Test + void writeDouble() { + + writer.writeDouble("double", 42.24D); + + assertThat(buffer).isEqualToIgnoringWhitespace("'double':{'$numberDouble':'42.24'}"); + } + + @Test + void writeDecimal128() { + + writer.writeDecimal128("decimal128", new Decimal128(128L)); + + assertThat(buffer).isEqualToIgnoringWhitespace("'decimal128':{'$numberDecimal':'128'}"); + } + + @Test + void writeObjectId() { + + ObjectId objectId = new ObjectId(); + writer.writeObjectId("_id", objectId); + + assertThat(buffer).isEqualToIgnoringWhitespace("'_id':{'$oid':'%s'}".formatted(objectId.toHexString())); + } + + @Test + void writeRegex() { + + String pattern = "^H"; + writer.writeRegularExpression("name", new BsonRegularExpression(pattern)); + + assertThat(buffer).isEqualToIgnoringWhitespace("'name':{'$regex':/%s/}".formatted(pattern)); + } + + @Test + void writeRegexWithOptions() { + + String pattern = "^H"; + writer.writeRegularExpression("name", new BsonRegularExpression(pattern, "i")); + + assertThat(buffer).isEqualToIgnoringWhitespace("'name':{'$regex':/%s/,'$options':'%s'}".formatted(pattern, "i")); + } + + @Test + void writeTimestamp() { + + writer.writeTimestamp("ts", new BsonTimestamp(1234, 567)); + + assertThat(buffer).isEqualToIgnoringWhitespace("'ts':{'$timestamp':{'t':1234,'i':567}}"); + } + + @Test + void writeUndefined() { + + writer.writeUndefined("nope"); + + assertThat(buffer).isEqualToIgnoringWhitespace("'nope':{'$undefined':true}"); + } + + @Test + void writeArrayWithSingleEntry() { + + writer.writeStartArray(); + writer.writeInt32(42); + writer.writeEndArray(); + + assertThat(buffer).isEqualToIgnoringNewLines("[{'$numberInt':'42'}]"); + } + + @Test + void writeArrayWithMultipleEntries() { + + writer.writeStartArray(); + writer.writeInt32(42); + writer.writeInt64(24); + writer.writeEndArray(); + + assertThat(buffer).isEqualToIgnoringNewLines("[{'$numberInt':'42'},{'$numberLong':'24'}]"); + } + +} diff --git a/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/ReactiveFindOperationExtensionsTests.kt b/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/ReactiveFindOperationExtensionsTests.kt index cbb7ae46f3..99d57002e4 100644 --- a/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/ReactiveFindOperationExtensionsTests.kt +++ b/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/ReactiveFindOperationExtensionsTests.kt @@ -270,9 +270,9 @@ class ReactiveFindOperationExtensionsTests { fun terminatingFindNearAllAsFlow() { val spec = mockk>() - val foo = GeoResult("foo", Distance(0.0)) - val bar = GeoResult("bar", Distance(0.0)) - val baz = GeoResult("baz", Distance(0.0)) + val foo = GeoResult("foo", Distance.of(0.0)) + val bar = GeoResult("bar", Distance.of(0.0)) + val baz = GeoResult("baz", Distance.of(0.0)) every { spec.all() } returns Flux.just(foo, bar, baz) runBlocking { diff --git a/spring-data-mongodb/src/test/resources/logback.xml b/spring-data-mongodb/src/test/resources/logback.xml index 64550c957c..55e4309a36 100644 --- a/spring-data-mongodb/src/test/resources/logback.xml +++ b/spring-data-mongodb/src/test/resources/logback.xml @@ -19,6 +19,9 @@ + + + diff --git a/spring-data-mongodb/src/test/resources/server-jmx.xml b/spring-data-mongodb/src/test/resources/server-jmx.xml deleted file mode 100644 index 54f985f4cb..0000000000 --- a/spring-data-mongodb/src/test/resources/server-jmx.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/antora/antora-playbook.yml b/src/main/antora/antora-playbook.yml index 9f842fe401..497f39ee04 100644 --- a/src/main/antora/antora-playbook.yml +++ b/src/main/antora/antora-playbook.yml @@ -17,7 +17,7 @@ content: - url: https://github.com/spring-projects/spring-data-commons # Refname matching: # https://docs.antora.org/antora/latest/playbook/content-refname-matching/ - branches: [ main, 3.3.x, 3.2.x] + branches: [ main, 4.0.x ] start_path: src/main/antora asciidoc: attributes: diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc index 221f47c011..6f2d1e2847 100644 --- a/src/main/antora/modules/ROOT/nav.adoc +++ b/src/main/antora/modules/ROOT/nav.adoc @@ -45,6 +45,7 @@ ** xref:repositories/create-instances.adoc[] ** xref:repositories/query-methods-details.adoc[] ** xref:mongodb/repositories/query-methods.adoc[] +** xref:mongodb/repositories/vector-search.adoc[] ** xref:mongodb/repositories/modifying-methods.adoc[] ** xref:repositories/projections.adoc[] ** xref:repositories/custom-implementations.adoc[] @@ -53,6 +54,7 @@ ** xref:mongodb/repositories/cdi-integration.adoc[] ** xref:repositories/query-keywords-reference.adoc[] ** xref:repositories/query-return-types-reference.adoc[] +** xref:mongodb/aot.adoc[] // Observability * xref:observability/observability.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/mongodb/aot.adoc b/src/main/antora/modules/ROOT/pages/mongodb/aot.adoc new file mode 100644 index 0000000000..16dd2f9ca0 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/mongodb/aot.adoc @@ -0,0 +1,85 @@ += Ahead of Time Optimizations + +This chapter covers Spring Data's Ahead of Time (AOT) optimizations that build upon {spring-framework-docs}/core/aot.html[Spring's Ahead of Time Optimizations]. + +[[aot.hints]] +== Runtime Hints + +Running an application as a native image requires additional information compared to a regular JVM runtime. +Spring Data contributes {spring-framework-docs}/core/aot.html#aot.hints[Runtime Hints] during AOT processing for native image usage. +These are in particular hints for: + +* Auditing +* `ManagedTypes` to capture the outcome of class-path scans +* Repositories +** Reflection hints for entities, return types, and Spring Data annotations +** Repository fragments +** Querydsl `Q` classes +** Kotlin Coroutine support +* Web support (Jackson Hints for `PagedModel`) + +[[aot.repositories]] +== Ahead of Time Repositories + +AOT Repositories are an extension to AOT processing by pre-generating eligible query method implementations. +Query methods are opaque to developers regarding their underlying queries being executed in a query method call. +AOT repositories contribute query method implementations based on derived or annotated queries, updates or aggregations that are known at build-time. +This optimization moves query method processing from runtime to build-time, which can lead to a significant bootstrap performance improvement as query methods do not need to be analyzed reflectively upon each application start. + +The resulting AOT repository fragment follows the naming scheme of `Impl__Aot` and is placed in the same package as the repository interface. +You can find all queries in their MQL form for generated repository query methods. + +[TIP] +==== +`spring.aot.repositories.enabled` property needs to be set to `true` for repository fragment code generation. +==== + +[NOTE] +==== +Consider AOT repository classes an internal optimization. +Do not use them directly in your code as generation and implementation details may change in future releases. +==== + +=== Running with AOT Repositories + +AOT is a mandatory step to transform a Spring application to a native executable, so it is automatically enabled when running in this mode. +It is also possible to use those optimizations on the JVM by setting the `spring.aot.enabled` and `spring.aot.repositories.enabled` System properties to `true`. + +AOT repositories contribute configuration changes to the actual repository bean registration to register the generated repository fragment. + +[NOTE] +==== +When AOT optimizations are included, some decisions that have been taken at build-time are hard-coded in the application setup. +For instance, profiles that have been enabled at build-time are automatically enabled at runtime as well. +Also, the Spring Data module implementing a repository is fixed. +Changing the implementation requires AOT re-processing. +==== + +=== Eligible Methods in Data MongoDB + +AOT repositories filter methods that are eligible for AOT processing. +These are typically all query methods that are not backed by an xref:repositories/custom-implementations.adoc[implementation fragment]. + +**Supported Features** + +* Derived `find`, `count`, `exists` and `delete` methods +* Query methods annotated with `@Query` (excluding those containing SpEL) +* Methods annotated with `@Aggregation` +* Methods using `@Update` +* `@Hint`, `@Meta`, and `@ReadPreference` support +* `Page`, `Slice`, and `Optional` return types +* DTO Projections + +**Limitations** + +* `@Meta.allowDiskUse` and `flags` are not evaluated. +* Queries / Aggregations / Updates containing `SpEL` cannot be generated. +* Limited `Collation` detection. + +**Excluded methods** + +* `CrudRepository` and other base interface methods +* Querydsl and Query by Example methods +* Methods whose implementation would be overly complex +* Query Methods obtaining MQL from a file +** Geospatial Queries diff --git a/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc b/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc index d76266c36a..3b5b4e49fe 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc @@ -465,7 +465,7 @@ This can be a single value (the _id_ by default), or a `Document` provided via a * `@Transient`: By default, all fields are mapped to the document. This annotation excludes the field where it is applied from being stored in the database. Transient properties cannot be used within a persistence constructor as the converter cannot materialize a value for the constructor argument. -* `@PersistenceConstructor`: Marks a given constructor - even a package protected one - to use when instantiating the object from the database. +* `@PersistenceCreator`: Marks a given constructor or a `static` factory method - even a package protected one - to use when instantiating the object from the database. Constructor arguments are mapped by name to the key values in the retrieved Document. * `@Value`: This annotation is part of the Spring Framework . Within the mapping framework it can be applied to constructor arguments. This lets you use a Spring Expression Language statement to transform a key's value retrieved in the database before it is used to construct a domain object. @@ -513,7 +513,7 @@ public class Person { this.ssn = ssn; } - @PersistenceConstructor + @PersistenceCreator public Person(Integer ssn, String firstName, String lastName, Integer age, T address) { this.ssn = ssn; this.firstName = firstName; @@ -673,7 +673,7 @@ Increased levels of nesting increase the complexity of the aggregation expressio [[mapping-custom-object-construction]] === Customized Object Construction -The mapping subsystem allows the customization of the object construction by annotating a constructor with the `@PersistenceConstructor` annotation. +The mapping subsystem allows the customization of the object construction by annotating a constructor with the `@PersistenceCreator` annotation. The values to be used for the constructor parameters are resolved in the following way: * If a parameter is annotated with the `@Value` annotation, the given expression is evaluated and the result is used as the parameter value. @@ -706,7 +706,7 @@ OrderItem item = converter.read(OrderItem.class, input); NOTE: The SpEL expression in the `@Value` annotation of the `quantity` parameter falls back to the value `0` if the given property path cannot be resolved. -Additional examples for using the `@PersistenceConstructor` annotation can be found in the https://github.com/spring-projects/spring-data-mongodb/blob/master/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java[MappingMongoConverterUnitTests] test suite. +Additional examples for using the `@PersistenceCreator` annotation can be found in the https://github.com/spring-projects/spring-data-mongodb/blob/master/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java[MappingMongoConverterUnitTests] test suite. [[mapping-usage-events]] === Mapping Framework Events diff --git a/src/main/antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc b/src/main/antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc index 98a6d2478a..14e866cf14 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc @@ -1,8 +1,8 @@ [[mongo.encryption]] -= Encryption (CSFLE) += Encryption Client Side Encryption is a feature that encrypts data in your application before it is sent to MongoDB. -We recommend you get familiar with the concepts, ideally from the https://www.mongodb.com/docs/manual/core/csfle/[MongoDB Documentation] to learn more about its capabilities and restrictions before you continue applying Encryption through Spring Data. +We recommend you get familiar with the concepts, ideally from the https://www.mongodb.com/docs/manual/core/security-in-use-encryption/[MongoDB Documentation] to learn more about its capabilities and restrictions before you continue applying Encryption through Spring Data. [NOTE] ==== @@ -11,8 +11,13 @@ MongoDB does not support encryption for all field types. Specific data types require deterministic encryption to preserve equality comparison functionality. ==== +== Client Side Field Level Encryption (CSFLE) + +Choosing CSFLE gives you full flexibility and allows you to use different keys for a single field, eg. in a one key per tenant scenario. + +Please make sure to consult the https://www.mongodb.com/docs/manual/core/csfle/[MongoDB CSFLE Documentation] before you continue reading. + [[mongo.encryption.automatic]] -== Automatic Encryption +=== Automatic Encryption (CSFLE) MongoDB supports https://www.mongodb.com/docs/manual/core/csfle/[Client-Side Field Level Encryption] out of the box using the MongoDB driver with its Automatic Encryption feature. Automatic Encryption requires a xref:mongodb/mapping/mapping-schema.adoc[JSON Schema] that allows to perform encrypted read and write operations without the need to provide an explicit en-/decryption step. @@ -47,7 +52,7 @@ MongoClientSettingsBuilderCustomizer customizer(MappingContext mappingContext) { ---- [[mongo.encryption.explicit]] -== Explicit Encryption +=== Explicit Encryption (CSFLE) Explicit encryption uses the MongoDB driver's encryption library (`org.mongodb:mongodb-crypt`) to perform encryption and decryption tasks. The `@ExplicitEncrypted` annotation is a combination of the `@Encrypted` annotation used for xref:mongodb/mapping/mapping-schema.adoc#mongo.jsonSchema.encrypted-fields[JSON Schema creation] and a xref:mongodb/mapping/property-converters.adoc[Property Converter]. @@ -114,8 +119,147 @@ By default, the `@ExplicitEncrypted(value=…)` attribute references a `MongoEnc It is possible to change the default implementation and exchange it with any `PropertyValueConverter` implementation by providing the according type reference. To learn more about custom `PropertyValueConverters` and the required configuration, please refer to the xref:mongodb/mapping/property-converters.adoc[Property Converters - Mapping specific fields] section. +[[mongo.encryption.queryable]] +== Queryable Encryption (QE) + +Choosing QE enables you to run different types of queries, like _range_ or _equality_, against encrypted fields. + +Please make sure to consult the https://www.mongodb.com/docs/manual/core/queryable-encryption/[MongoDB QE Documentation] before you continue reading to learn more about QE features and limitations. + +=== Collection Setup + +Queryable Encryption requires upfront declaration of certain aspects allowed within an actual query against an encrypted field. +The information covers the algorithm in use as well as allowed query types along with their attributes and must be provided when creating the collection. + +`MongoOperations#createCollection(...)` can be used to do the initial setup for collections utilizing QE. +The configuration for QE via Spring Data uses the same building blocks (a xref:mongodb/mapping/mapping-schema.adoc#mongo.jsonSchema.encrypted-fields[JSON Schema creation]) as CSFLE, converting the schema/properties into the configuration format required by MongoDB. + +[tabs] +====== +Manual Collection Setup:: ++ +==== +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- +CollectionOptions collectionOptions = CollectionOptions.encryptedCollection(options -> options + .queryable(encrypted(string("ssn")).algorithm("Indexed"), equality().contention(0)) + .queryable(encrypted(int32("age")).algorithm("Range"), range().contention(8).min(0).max(150)) + .queryable(encrypted(int64("address.sign")).algorithm("Range"), range().contention(2).min(-10L).max(10L)) +); + +mongoTemplate.createCollection(Patient.class, collectionOptions); <1> +---- +<1> Using the template to create the collection may prevent capturing generated keyIds. In this case render the `Document` from the options and use the `createEncryptedCollection(...)` method via the encryption library. +==== + +Derived Collection Setup:: ++ +==== +[source,java,indent=0,subs="verbatim,quotes",role="secondary"] +---- +class Patient { + + @Id String id; + + @Encrypted(algorithm = "Indexed") // + @Queryable(queryType = "equality", contentionFactor = 0) + String ssn; + + @RangeEncrypted(contentionFactor = 8, rangeOptions = "{ 'min' : 0, 'max' : 150 }") + Integer age; + + Address address; +} + +MongoJsonSchema patientSchema = MongoJsonSchemaCreator.create(mappingContext) + .filter(MongoJsonSchemaCreator.encryptedOnly()) + .createSchemaFor(Patient.class); + +CollectionOptions collectionOptions = CollectionOptions.encryptedCollection(patientSchema); + +mongoTemplate.createCollection(Patient.class, collectionOptions); <1> +---- +<1> Using the template to create the collection may prevent capturing generated keyIds. In this case render the `Document` from the options and use the `createEncryptedCollection(...)` method via the encryption library. + +The `Queryable` annotation allows to define allowed query types for encrypted fields. +`@RangeEncrypted` is a combination of `@Encrypted` and `@Queryable` for fields allowing `range` queries. +It is possible to create custom annotations out of the provided ones. +==== + +MongoDB Collection Info:: ++ +==== +[source,java,indent=0,subs="verbatim,quotes",role="thrid"] +---- +{ + name: 'patient', + type: 'collection', + options: { + encryptedFields: { + escCollection: 'enxcol_.test.esc', + ecocCollection: 'enxcol_.test.ecoc', + fields: [ + { + keyId: ..., + path: 'ssn', + bsonType: 'string', + queries: [ { queryType: 'equality', contention: Long('0') } ] + }, + { + keyId: ..., + path: 'age', + bsonType: 'int', + queries: [ { queryType: 'range', contention: Long('8'), min: 0, max: 150 } ] + }, + { + keyId: ..., + path: 'address.sign', + bsonType: 'long', + queries: [ { queryType: 'range', contention: Long('2'), min: Long('-10'), max: Long('10') } ] + } + ] + } + } +} +---- +==== +====== + +[NOTE] +==== +- It is not possible to use both QE and CSFLE within the same collection. +- It is not possible to query a `range` indexed field with an `equality` operator. +- It is not possible to query an `equality` indexed field with a `range` operator. +- It is not possible to set `bypassAutoEncrytion(true)`. +- It is not possible to use self maintained encryption keys via `@Encrypted` in combination with Queryable Encryption. +- Contention is only optional on the server side, the clients requires you to set the value (Default us `8`). +- Additional options for eg. `min` and `max` need to match the actual field type. Make sure to use `$numberLong` etc. to ensure target types when parsing bson String. +- Queryable Encryption will an extra field `__safeContent__` to each of your documents. +Unless explicitly excluded the field will be loaded into memory when retrieving results. +==== + +[[mongo.encryption.queryable.automatic]] +=== Automatic Encryption (QE) + +MongoDB supports Queryable Encryption out of the box using the MongoDB driver with its Automatic Encryption feature. +Automatic Encryption requires a xref:mongodb/mapping/mapping-schema.adoc[JSON Schema] that allows to perform encrypted read and write operations without the need to provide an explicit en-/decryption step. + +All you need to do is create the collection according to the MongoDB documentation. +You may utilize techniques to create the required configuration outlined in the section above. + +[[mongo.encryption.queryable.manual]] +=== Explicit Encryption (QE) + +Explicit encryption uses the MongoDB driver's encryption library (`org.mongodb:mongodb-crypt`) to perform encryption and decryption tasks based on the meta information provided by annotation within the domain model. + +[NOTE] +==== +There is no official support for using Explicit Queryable Encryption. +The audacious user may combine `@Encrypted` and `@Queryable` with `@ValueConverter(MongoEncryptionConverter.class)` at their own risk. +==== + [[mongo.encryption.explicit-setup]] -=== MongoEncryptionConverter Setup +[[mongo.encryption.converter-setup]] +== MongoEncryptionConverter Setup The converter setup for `MongoEncryptionConverter` requires a few steps as several components are involved. The bean setup consists of the following: @@ -124,7 +268,6 @@ The bean setup consists of the following: 2. A `MongoEncryptionConverter` instance configured with `ClientEncryption` and a `EncryptionKeyResolver`. 3. A `PropertyValueConverterFactory` that uses the registered `MongoEncryptionConverter` bean. -A side effect of using annotated key resolution is that the `@ExplicitEncrypted` annotation does not need to specify an alt key name. The `EncryptionKeyResolver` uses an `EncryptionContext` providing access to the property allowing for dynamic DEK resolution. .Sample MongoEncryptionConverter Configuration diff --git a/src/main/antora/modules/ROOT/pages/mongodb/mongo-search-indexes.adoc b/src/main/antora/modules/ROOT/pages/mongodb/mongo-search-indexes.adoc index 9b6bfcf095..7fc51de007 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/mongo-search-indexes.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/mongo-search-indexes.adoc @@ -25,7 +25,7 @@ Java:: [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- VectorIndex index = new VectorIndex("vector_index") - .addVector("plotEmbedding"), vector -> vector.dimensions(1536).similarity(COSINE)) <1> + .addVector("plotEmbedding", vector -> vector.dimensions(1536).similarity(COSINE)) <1> .addFilter("year"); <2> mongoTemplate.searchIndexOps(Movie.class) <3> @@ -84,7 +84,6 @@ VectorSearchOperation search = VectorSearchOperation.search("vector_index") <1> .vector( ... ) .numCandidates(150) .limit(10) - .quantization(SCALAR) .withSearchScore("score"); <3> AggregationResults results = mongoTemplate @@ -107,8 +106,7 @@ db.embedded_movies.aggregate([ "path": "plot_embedding", <1> "queryVector": [ ... ], "numCandidates": 150, - "limit": 10, - "quantization": "scalar" + "limit": 10 } }, { diff --git a/src/main/antora/modules/ROOT/pages/mongodb/repositories/vector-search.adoc b/src/main/antora/modules/ROOT/pages/mongodb/repositories/vector-search.adoc new file mode 100644 index 0000000000..9129c80a21 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/mongodb/repositories/vector-search.adoc @@ -0,0 +1,8 @@ +:vector-search-intro-include: partial$vector-search-intro-include.adoc +:vector-search-model-include: partial$vector-search-model-include.adoc +:vector-search-repository-include: partial$vector-search-repository-include.adoc +:vector-search-scoring-include: partial$vector-search-scoring-include.adoc +:vector-search-method-derived-include: partial$vector-search-method-derived-include.adoc +:vector-search-method-annotated-include: partial$vector-search-method-annotated-include.adoc + +include::partial$/vector-search.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc b/src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc index f2a7a19bd6..ece61559ec 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc @@ -116,6 +116,19 @@ WARNING: Projections must not be applied to xref:mongodb/mapping/document-refere You can switch between retrieving a single entity and retrieving multiple entities as a `List` or a `Stream` through the terminating methods: `first()`, `one()`, `all()`, or `stream()`. +Results can be contextually post-processed by using a `QueryResultConverter` that has access to both the raw result `Document` and the already mapped object by calling `map(...)` as outlined below. + +[source,java] +==== +---- +List> result = template.query(Person.class) + .as(Jedi.class) + .matching(query(where("firstname").is("luke"))) + .map((document, reader) -> Optional.of(reader.get())) + .all(); +---- +==== + When writing a geo-spatial query with `near(NearQuery)`, the number of terminating methods is altered to include only the methods that are valid for running a `geoNear` command in MongoDB (fetching entities as a `GeoResult` within `GeoResults`), as the following example shows: [tabs] diff --git a/src/main/antora/modules/ROOT/pages/mongodb/template-query-operations.adoc b/src/main/antora/modules/ROOT/pages/mongodb/template-query-operations.adoc index a424748205..697af23a9e 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/template-query-operations.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/template-query-operations.adoc @@ -342,7 +342,7 @@ public class Venue { private String name; private double[] location; - @PersistenceConstructor + @PersistenceCreator Venue(String name, double[] location) { super(); this.name = name; diff --git a/src/main/antora/modules/ROOT/pages/repositories/core-concepts.adoc b/src/main/antora/modules/ROOT/pages/repositories/core-concepts.adoc index 1a4af7a60b..7d31acb2d4 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/core-concepts.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/core-concepts.adoc @@ -2,11 +2,3 @@ include::{commons}@data-commons::page$repositories/core-concepts.adoc[] [[mongodb.entity-persistence.state-detection-strategies]] include::{commons}@data-commons::page$is-new-state-detection.adoc[leveloffset=+1] - -[NOTE] -==== -Cassandra provides no means to generate identifiers upon inserting data. -As consequence, entities must be associated with identifier values. -Spring Data defaults to identifier inspection to determine whether an entity is new. -If you want to use xref:mongodb/auditing.adoc[auditing] make sure to either use xref:mongodb/template-crud-operations.adoc#mongo-template.optimistic-locking[Optimistic Locking] or implement `Persistable` for proper entity state detection. -==== diff --git a/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc b/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc index 3bc7648154..75dcea1e4f 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc @@ -169,7 +169,6 @@ Maven:: com.querydsl querydsl-mongodb ${querydslVersion} - jakarta @@ -216,7 +215,7 @@ Gradle:: [source,groovy,indent=0,subs="verbatim,quotes",role="secondary"] ---- dependencies { - implementation 'com.querydsl:querydsl-mongodb:${querydslVersion}:jakarta' + implementation 'com.querydsl:querydsl-mongodb:${querydslVersion}' annotationProcessor 'com.querydsl:querydsl-apt:${querydslVersion}:jakarta' annotationProcessor 'org.springframework.data:spring-data-mongodb' @@ -235,6 +234,8 @@ tasks.withType(JavaCompile).configureEach { ====== Note that the setup above shows the simplest usage omitting any other options or dependencies that your project might require. +This way of configuring annotation processing disables Java's annotation processor scanning because MongoDB requires specifying `-processor` by class name. +If you're using other annotation processors, you need to add them to the list of `-processor`/`annotationProcessors` as well. include::{commons}@data-commons::page$repositories/core-extensions-web.adoc[leveloffset=1] diff --git a/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc b/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc index dfe4814955..614da0b059 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc @@ -1 +1,2 @@ +:feature-scroll: include::{commons}@data-commons::page$repositories/query-methods-details.adoc[] diff --git a/src/main/antora/modules/ROOT/partials/vector-search-intro-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-intro-include.adoc new file mode 100644 index 0000000000..355bccf4e3 --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search-intro-include.adoc @@ -0,0 +1 @@ +To use Vector Search with MongoDB, you need a MongoDB Atlas instance that is either running in the cloud or by using https://www.mongodb.com/docs/atlas/cli/current/atlas-cli-deploy-docker/[Docker]. diff --git a/src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc new file mode 100644 index 0000000000..252437f0b7 --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc @@ -0,0 +1,23 @@ +Annotated search methods use the `@VectorSearch` annotation to define parameters for the https://www.mongodb.com/docs/upcoming/reference/operator/aggregation/vectorSearch/[`$vectorSearch`] aggregation stage. + +.Using `@VectorSearch` Search Methods +==== +[source,java] +---- +interface CommentRepository extends Repository { + + @VectorSearch(indexName = "cos-index", filter = "{country: ?0}", limit="100", numCandidates="2000") + SearchResults searchAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, + Score distance); + + @VectorSearch(indexName = "my-index", filter = "{country: ?0}", limit="?3", numCandidates = "#{#limit * 20}", + searchType = VectorSearchOperation.SearchType.ANN) + List findAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, Score distance, int limit); +} +---- +==== + +Annotated Search Methods can define `filter` for pre-filter usage. + +`filter`, `limit`, and `numCandidates` support xref:page$mongodb/value-expressions.adoc[Value Expressions] allowing references to search method arguments. + diff --git a/src/main/antora/modules/ROOT/partials/vector-search-method-derived-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-method-derived-include.adoc new file mode 100644 index 0000000000..f2b006b8e4 --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search-method-derived-include.adoc @@ -0,0 +1,21 @@ +MongoDB Search methods must use the `@VectorSearch` annotation to define the index name for the https://www.mongodb.com/docs/upcoming/reference/operator/aggregation/vectorSearch/[`$vectorSearch`] aggregation stage. + +.Using `Near` and `Within` Keywords in Repository Search Methods +==== +[source,java] +---- +interface CommentRepository extends Repository { + + @VectorSearch(indexName = "my-index", numCandidates="200") + SearchResults searchTop10ByEmbeddingNear(Vector vector, Score score); + + @VectorSearch(indexName = "my-index", numCandidates="200") + SearchResults searchTop10ByEmbeddingWithin(Vector vector, Range range); + + @VectorSearch(indexName = "my-index", numCandidates="200") + SearchResults searchTop10ByCountryAndEmbeddingWithin(String country, Vector vector, Range range); +} +---- +==== + +Derived Search Methods can define domain model attributes to create the pre-filter for indexed fields. diff --git a/src/main/antora/modules/ROOT/partials/vector-search-model-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-model-include.adoc new file mode 100644 index 0000000000..e657f3aa63 --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search-model-include.adoc @@ -0,0 +1,15 @@ +==== +[source,java] +---- +class Comment { + + @Id String id; + String country; + String comment; + + Vector embedding; + + // getters, setters, … +} +---- +==== diff --git a/src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc new file mode 100644 index 0000000000..0e987fc1c5 --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc @@ -0,0 +1,25 @@ +.Using `SearchResult` in a Repository Search Method +==== +[source,java] +---- +interface CommentRepository extends Repository { + + @VectorSearch(indexName = "my-index", numCandidates="#{#limit.max() * 20}") + SearchResults searchByCountryAndEmbeddingNear(String country, Vector vector, Score score, + Limit limit); + + @VectorSearch(indexName = "my-index", limit="10", numCandidates="200") + SearchResults searchByCountryAndEmbeddingWithin(String country, Vector embedding, + Score score); + +} + +SearchResults results = repository.searchByCountryAndEmbeddingNear("en", Vector.of(…), Score.of(0.9), Limit.of(10)); +---- +==== + +[TIP] +==== +The MongoDB https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/[vector search aggregation] stage defines a set of required arguments and restrictions. +Please make sure to follow the guidelines and make sure to provide required arguments like `limit`. +==== diff --git a/src/main/antora/modules/ROOT/partials/vector-search-scoring-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-scoring-include.adoc new file mode 100644 index 0000000000..313d8bf394 --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search-scoring-include.adoc @@ -0,0 +1,32 @@ +MongoDB reports the score directly as similarity value. +The scoring function must be specified in the index and therefore, Vector search methods do not consider the `Score.scoringFunction`. +The scoring function defaults to `ScoringFunction.unspecified()` as there is no information inside of search results how the score has been computed. + +.Using `Score` and `Similarity` in a Repository Search Methods +==== +[source,java] +---- +interface CommentRepository extends Repository { + + @VectorSearch(…) + SearchResults searchTop10ByEmbeddingNear(Vector vector, Score similarity); + + @VectorSearch(…) + SearchResults searchTop10ByEmbeddingNear(Vector vector, Similarity similarity); + + @VectorSearch(…) + SearchResults searchTop10ByEmbeddingNear(Vector vector, Range range); +} + +repository.searchByEmbeddingNear(Vector.of(…), Score.of(0.9)); <1> + +repository.searchByEmbeddingNear(Vector.of(…), Similarity.of(0.9)); <2> + +repository.searchByEmbeddingNear(Vector.of(…), Similarity.between(0.5, 1)); <3> +---- + +<1> Run a search and return results with a similarity of `0.9` or greater. +<2> Return results with a similarity of `0.9` or greater. +<3> Return results with a similarity of between `0.5` and `1.0` or greater. +==== + diff --git a/src/main/antora/modules/ROOT/partials/vector-search.adoc b/src/main/antora/modules/ROOT/partials/vector-search.adoc new file mode 100644 index 0000000000..15e32dccee --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search.adoc @@ -0,0 +1,167 @@ +[[vector-search]] += Vector Search + +With the rise of Generative AI, Vector databases have gained strong traction in the world of databases. +These databases enable efficient storage and querying of high-dimensional vectors, making them well-suited for tasks such as semantic search, recommendation systems, and natural language understanding. + +Vector search is a technique that retrieves semantically similar data by comparing vector representations (also known as embeddings) rather than relying on traditional exact-match queries. +This approach enables intelligent, context-aware applications that go beyond keyword-based retrieval. + +In the context of Spring Data, vector search opens new possibilities for building intelligent, context-aware applications, particularly in domains like natural language processing, recommendation systems, and generative AI. +By modelling vector-based querying using familiar repository abstractions, Spring Data allows developers to seamlessly integrate similarity-based vector-capable databases with the simplicity and consistency of the Spring Data programming model. + +ifdef::vector-search-intro-include[] +include::{vector-search-intro-include}[] +endif::[] + +[[vector-search.model]] +== Vector Model + +To support vector search in a type-safe and idiomatic way, Spring Data introduces the following core abstractions: + +* <> +* <` and `SearchResult`>> +* <> + +[[vector-search.model.vector]] +=== `Vector` + +The `Vector` type represents an n-dimensional numerical embedding, typically produced by embedding models. +In Spring Data, it is defined as a lightweight wrapper around an array of floating-point numbers, ensuring immutability and consistency. +This type can be used as an input for search queries or as a property on a domain entity to store the associated vector representation. + +==== +[source,java] +---- +Vector vector = Vector.of(0.23f, 0.11f, 0.77f); +---- +==== + +Using `Vector` in your domain model removes the need to work with raw arrays or lists of numbers, providing a more type-safe and expressive way to handle vector data. +This abstraction also allows for easy integration with various vector databases and libraries. +It also allows for implementing vendor-specific optimizations such as binary or quantized vectors that do not map to a standard floating point (`float` and `double` as of https://en.wikipedia.org/wiki/IEEE_754[IEEE 754]) representation. +A domain object can have a vector property, which can be used for similarity searches. +Consider the following example: + +ifdef::vector-search-model-include[] +include::{vector-search-model-include}[] +endif::[] + +NOTE: Associating a vector with a domain object results in the vector being loaded and stored as part of the entity lifecycle, which may introduce additional overhead on retrieval and persistence operations. + +[[vector-search.model.search-result]] +=== Search Results + +The `SearchResult` type encapsulates the results of a vector similarity query. +It includes both the matched domain object and a relevance score that indicates how closely it matches the query vector. +This abstraction provides a structured way to handle result ranking and enables developers to easily work with both the data and its contextual relevance. + +ifdef::vector-search-repository-include[] +include::{vector-search-repository-include}[] +endif::[] + +In this example, the `searchByCountryAndEmbeddingNear` method returns a `SearchResults` object, which contains a list of `SearchResult` instances. +Each result includes the matched `Comment` entity and its relevance score. + +Relevance score is a numerical value that indicates how closely the matched vector aligns with the query vector. +Depending on whether a score represents distance or similarity a higher score can mean a closer match or a more distant one. + +The scoring function used to calculate this score can vary based on the underlying database, index or input parameters. + +[[vector-search.model.scoring]] +=== Score, Similarity, and Scoring Functions + +The `Score` type holds a numerical value indicating the relevance of a search result. +It can be used to rank results based on their similarity to the query vector. +The `Score` type is typically a floating-point number, and its interpretation (higher is better or lower is better) depends on the specific similarity function used. +Scores are a by-product of vector search and are not required for a successful search operation. +Score values are not part of a domain model and therefore represented best as out-of-band data. + +Generally, a Score is computed by a `ScoringFunction`. +The actual scoring function used to calculate this score can depends on the underlying database and can be obtained from a search index or input parameters. + +Spring Data support declares constants for commonly used functions such as: + +Euclidean Distance:: Calculates the straight-line distance in n-dimensional space involving the square root of the sum of squared differences. +Cosine Similarity:: Measures the angle between two vectors by calculating the Dot product first and then normalizing its result by dividing by the product of their lengths. +Dot Product:: Computes the sum of element-wise multiplications. + +The choice of similarity function can impact both the performance and semantics of the search and is often determined by the underlying database or index being used. +Spring Data adopts to the database's native scoring function capabilities and whether the score can be used to limit results. + +ifdef::vector-search-scoring-include[] +include::{vector-search-scoring-include}[] +endif::[] + +[[vector-search.methods]] +== Vector Search Methods + +Vector search methods are defined in repositories using the same conventions as standard Spring Data query methods. +These methods return `SearchResults` and require a `Vector` parameter to define the query vector. +The actual implementation depends on the actual internals of the underlying data store and its capabilities around vector search. + +NOTE: If you are new to Spring Data repositories, make sure to familiarize yourself with the xref:repositories/core-concepts.adoc[basics of repository definitions and query methods]. + +Generally, you have the choice of declaring a search method using two approaches: + +* Query Derivation +* Declaring a String-based Query + +Vector Search methods must declare a `Vector` parameter to define the query vector. + +[[vector-search.method.derivation]] +=== Derived Search Methods + +A derived search method uses the name of the method to derive the query. +Vector Search supports the following keywords to run a Vector search when declaring a search method: + +.Query predicate keywords +[options="header",cols="1,3"] +|=============== +|Logical keyword|Keyword expressions +|`NEAR`|`Near`, `IsNear` +|`WITHIN`|`Within`, `IsWithin` +|=============== + +ifdef::vector-search-method-derived-include[] +include::{vector-search-method-derived-include}[] +endif::[] + +Derived search methods are typically easier to read and maintain, as they rely on the method name to express the query intent. +However, a derived search method requires either to declare a `Score`, `Range` or `ScoreFunction` as second argument to the `Near`/`Within` keyword to limit search results by their score. + +[[vector-search.method.string]] +=== Annotated Search Methods + +Annotated methods provide full control over the query semantics and parameters. +Unlike derived methods, they do not rely on method name conventions. + +ifdef::vector-search-method-annotated-include[] +include::{vector-search-method-annotated-include}[] +endif::[] + +With more control over the actual query, Spring Data can make fewer assumptions about the query and its parameters. +For example, `Similarity` normalization uses the native score function within the query to normalize the given similarity into a score predicate value and vice versa. +If an annotated query does not define e.g. the score, then the score value in the returned `SearchResult` will be zero. + +[[vector-search.method.sorting]] +=== Sorting + +By default, search results are ordered according to their score. +You can override sorting by using the `Sort` parameter: + +.Using `Sort` in Repository Search Methods +==== +[source,java] +---- +interface CommentRepository extends Repository { + + SearchResults searchByEmbeddingNearOrderByCountry(Vector vector, Score score); + + SearchResults searchByEmbeddingWithin(Vector vector, Score score, Sort sort); +} +---- +==== + +Please note that custom sorting does not allow expressing the score as a sorting criteria. +You can only refer to domain properties. diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt index 61b472b23b..6deef0df9c 100644 --- a/src/main/resources/notice.txt +++ b/src/main/resources/notice.txt @@ -1,4 +1,4 @@ -Spring Data MongoDB 4.5 M2 (2025.0.0) +Spring Data MongoDB 5.0 M2 (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").