diff --git a/pom.xml b/pom.xml index ffbe2b8e19..e3f0e5a32e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.0-GH-3279-SNAPSHOT pom Spring Data MongoDB @@ -26,7 +26,7 @@ multi spring-data-mongodb - 4.0.0-SNAPSHOT + 4.0.x-GH-3279-SNAPSHOT 5.4.0 1.19 diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index fc88571622..258e48e323 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 - 5.0.0-SNAPSHOT + 5.0.0-GH-3279-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index ad3c1338ec..0f075706b8 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.0-GH-3279-SNAPSHOT ../pom.xml @@ -140,6 +140,13 @@ true + + net.javacrumbs.json-unit + json-unit-assertj + 4.1.0 + test + + 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/support/MongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java index e1abcdc2ab..4ff89c9fdb 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,15 +15,13 @@ */ 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; @@ -35,7 +33,6 @@ import org.springframework.data.mongodb.repository.query.StringBasedAggregation; import org.springframework.data.mongodb.repository.query.StringBasedMongoQuery; 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; @@ -60,6 +57,7 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport { private final CrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor(); private final MongoOperations operations; private final MappingContext, MongoPersistentProperty> mappingContext; + private MongoRepositoryFragmentsContributor fragmentsContributor = MongoRepositoryFragmentsContributor.DEFAULT; /** * Creates a new {@link MongoRepositoryFactory} with the given {@link MongoOperations}. @@ -76,6 +74,17 @@ 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(@Nullable ClassLoader classLoader) { @@ -99,33 +108,18 @@ 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.getDomainType()), operations); } @Override 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 cec54de0bb..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 @@ -30,12 +30,14 @@ * {@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; @@ -57,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. * @@ -76,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( @@ -92,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/ReactiveMongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java index ae8561bc17..c113c70a5b 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,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.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.ReactiveMongoOperations; @@ -34,13 +33,11 @@ import org.springframework.data.mongodb.repository.query.ReactiveStringBasedAggregation; import org.springframework.data.mongodb.repository.query.ReactiveStringBasedMongoQuery; 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; @@ -61,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; /** @@ -78,6 +76,17 @@ public ReactiveMongoRepositoryFactory(ReactiveMongoOperations mongoOperations) { 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(@Nullable ClassLoader classLoader) { @@ -96,24 +105,20 @@ 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.getDomainType(), metadata), + operations); } @Override 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 e3d71325f9..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 @@ -40,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; @@ -61,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. * @@ -81,7 +99,8 @@ public void setMappingContext(MappingContext mappingContext) { @SuppressWarnings("NullAway") protected RepositoryFactorySupport createRepositoryFactory() { - RepositoryFactorySupport factory = getFactoryInstance(operations); + ReactiveMongoRepositoryFactory factory = getFactoryInstance(operations); + factory.setFragmentsContributor(repositoryFragmentsContributor); if (createIndexesForQueryMethods) { factory.addQueryCreationListener(new IndexEnsuringQueryCreationListener( @@ -97,7 +116,7 @@ protected RepositoryFactorySupport createRepositoryFactory() { * @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/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/MongoRepositoryContributorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java index 1ec0c3609b..94b58200b4 100644 --- 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 @@ -15,9 +15,7 @@ */ package org.springframework.data.mongodb.repository.aot; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatException; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.*; import example.aot.User; import example.aot.UserProjection; @@ -32,6 +30,7 @@ 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; @@ -55,7 +54,7 @@ * @author Christoph Strobl */ @ExtendWith(MongoClientExtension.class) -@SpringJUnitConfig(classes = MongoRepositoryContributorTests.JpaRepositoryContributorConfiguration.class) +@SpringJUnitConfig(classes = MongoRepositoryContributorTests.MongoRepositoryContributorConfiguration.class) public class MongoRepositoryContributorTests { private static final String DB_NAME = "aot-repo-tests"; @@ -64,9 +63,9 @@ public class MongoRepositoryContributorTests { @Autowired UserRepository fragment; @Configuration - static class JpaRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport { + static class MongoRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport { - public JpaRepositoryContributorConfiguration() { + public MongoRepositoryContributorConfiguration() { super(UserRepository.class); } 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..fd0b051c1e --- /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-3830 + 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-3830 + 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-3830 + 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-3830 + 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-3830 + @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-3830 + 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-3830 + 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-3830 + void shouldDocumentBaseFragment() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + System.out.println(json); + 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..565b8a2052 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/ReactiveAotContributionIntegrationTests.java @@ -0,0 +1,117 @@ +/* + * 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 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-3830 + 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/TestMongoAotRepositoryContext.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java index 2349524fab..7cc47d9566 100644 --- 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 @@ -21,12 +21,13 @@ import java.util.Set; import org.jspecify.annotations.Nullable; -import org.springframework.core.env.Environment; -import org.springframework.core.env.StandardEnvironment; -import org.springframework.core.test.tools.ClassFile; + 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.core.RepositoryInformation; @@ -64,6 +65,11 @@ public String getBeanName() { return "dummyRepository"; } + @Override + public String getModuleName() { + return "MongoDB"; + } + @Override public Set getBasePackages() { return Set.of("org.springframework.data.dummy.repository.aot"); 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..6d38c5ba5e --- /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.*; +import static org.mockito.Mockito.*; + +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-3279 + 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/ReactiveMongoRepositoryFragmentsContributorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributorUnitTests.java new file mode 100644 index 0000000000..d3cc84672a --- /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.*; +import static org.mockito.Mockito.*; + +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-3279 + 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 {} + +}