From 70ac1af76fce222273a9a6c6a4cae1d22958dc31 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 8 Jan 2025 11:26:48 +0100 Subject: [PATCH 01/13] Prepare issue branch --- pom.xml | 4 ++-- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index c3245aad49..989ab0deb9 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0-SNAPSHOT + 4.5.x-GENERATED-REPOSITORIES-SNAPSHOT pom Spring Data MongoDB @@ -26,7 +26,7 @@ multi spring-data-mongodb - 3.5.0-SNAPSHOT + 3.5.x-GENERATED-REPOSITORIES-SNAPSHOT 5.2.1 ${mongo} ${mongo} diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 58c63dfc97..56d5b91242 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 + 4.5.x-GENERATED-REPOSITORIES-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 98516a5ba9..f32e9c4034 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 + 4.5.x-GENERATED-REPOSITORIES-SNAPSHOT ../pom.xml From 123a191a12345e298acfa645fb729145203cd1a8 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 9 Jan 2025 13:18:22 +0100 Subject: [PATCH 02/13] support anntotated aot generated repositories --- spring-data-mongodb/pom.xml | 6 + .../generated/MongoRepositoryContributor.java | 92 +++++++++++ .../aot/AotMongoRepositoryPostProcessor.java | 6 + .../CrudMethodMetadataPostProcessor.java | 3 +- .../src/test/java/example/aot/User.java | 102 +++++++++++++ .../test/java/example/aot/UserRepository.java | 32 ++++ .../data/mongodb/aot/generated/DemoRepo.java | 62 ++++++++ .../MongoRepositoryContributorTests.java | 60 ++++++++ .../generated/StubRepositoryInformation.java | 144 ++++++++++++++++++ .../TestMongoAotRepositoryContext.java | 121 +++++++++++++++ .../src/test/resources/logback.xml | 1 + 11 files changed, 628 insertions(+), 1 deletion(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java create mode 100644 spring-data-mongodb/src/test/java/example/aot/User.java create mode 100644 spring-data-mongodb/src/test/java/example/aot/UserRepository.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/DemoRepo.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/StubRepositoryInformation.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/TestMongoAotRepositoryContext.java diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index f32e9c4034..cdbb32b347 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -273,6 +273,12 @@ test + + org.springframework + spring-core-test + test + + org.jetbrains.kotlin diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java new file mode 100644 index 0000000000..216f67e6c5 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.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.aot.generated; + +import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.stream.Collectors; + +import org.apache.commons.logging.Log; +import org.springframework.core.annotation.AnnotatedElementUtils; +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.repository.Query; +import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder; +import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder; +import org.springframework.data.repository.aot.generate.RepositoryContributor; +import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.javapoet.TypeName; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public class MongoRepositoryContributor extends RepositoryContributor { + + public MongoRepositoryContributor(AotRepositoryContext repositoryContext) { + super(repositoryContext); + } + + @Override + protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) { + constructorBuilder.addParameter("operations", TypeName.get(MongoOperations.class)); + } + + @Override + protected void customizeDerivedMethod(AotRepositoryMethodBuilder methodBuilder) { + + methodBuilder.customize((repositoryInformation, metadata, body) -> { + Query query = AnnotatedElementUtils.findMergedAnnotation(metadata.getRepositoryMethod(), Query.class); + if (query != null) { + + String mongoOpsRef = metadata.fieldNameOf(MongoOperations.class); + String arguments = StringUtils + .collectionToCommaDelimitedString(Arrays.stream(metadata.getRepositoryMethod().getParameters()) + .map(Parameter::getName).collect(Collectors.toList())); + + body.beginControlFlow("if($L.isDebugEnabled())", metadata.fieldNameOf(Log.class)); + body.addStatement("$L.debug(\"invoking generated [$L] method\")", metadata.fieldNameOf(Log.class), + metadata.getRepositoryMethod().getName()); + body.endControlFlow(); + + body.addStatement("$T filter = new $T($S, $L.getConverter(), new $T[]{ $L })", BindableMongoExpression.class, + BindableMongoExpression.class, query.value(), mongoOpsRef, Object.class, arguments); + body.addStatement("$T query = new $T(filter.toDocument())", + org.springframework.data.mongodb.core.query.Query.class, BasicQuery.class); + + if (metadata.getActualReturnType() != null && ObjectUtils + .nullSafeEquals(TypeName.get(repositoryInformation.getDomainType()), metadata.getActualReturnType())) { + body.addStatement(""" + return $L.query($T.class) + .matching(query) + .all()""", mongoOpsRef, repositoryInformation.getDomainType()); + } else { + + body.addStatement(""" + return $L.query($T.class) + .as($T.class) + .matching(query) + .all()""", mongoOpsRef, repositoryInformation.getDomainType(), + metadata.getActualReturnType() != null ? metadata.getActualReturnType() + : repositoryInformation.getDomainType()); + } + } + }); + } +} 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..a780620fc9 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 @@ -16,8 +16,10 @@ package org.springframework.data.mongodb.repository.aot; 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.mongodb.aot.generated.MongoRepositoryContributor; import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor; import org.springframework.data.util.TypeContributor; @@ -39,6 +41,10 @@ protected void contribute(AotRepositoryContext repositoryContext, GenerationCont TypeContributor.contribute(type, it -> true, generationContext); lazyLoadingProxyAotProcessor.registerLazyLoadingProxyIfNeeded(type, generationContext); }); + + if (AotContext.aotGeneratedRepositoriesEnabled()) { + new MongoRepositoryContributor(repositoryContext).contribute(generationContext); + } } @Override 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..e430a010c9 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 @@ -39,6 +39,7 @@ 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. @@ -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())); } } 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..28ea5911ed --- /dev/null +++ b/spring-data-mongodb/src/test/java/example/aot/User.java @@ -0,0 +1,102 @@ +/* + * 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 + * + * http://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. + */ + +/* + * 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 + * + * http://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 + * @since 2025/01 + */ +public class User { + + String id; + + String username; + + @Field("first_name") String firstname; + + @Field("last_name") String lastname; + + Instant registrationDate; + Instant lastSeen; + + 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; + } +} 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..bda2e142f9 --- /dev/null +++ b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java @@ -0,0 +1,32 @@ +/* + * 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 + * + * http://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.util.List; + +import org.springframework.data.mongodb.repository.Query; +import org.springframework.data.repository.CrudRepository; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public interface UserRepository extends CrudRepository { + + + @Query("{ 'username' : '?0' }") + List findAllByAnnotatedQueryWithParameter(String username); +} 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..bef0d34cb4 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/DemoRepo.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 + * + * http://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. + */ + +/* + * 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 + * + * http://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/aot/generated/MongoRepositoryContributorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java new file mode 100644 index 0000000000..a24707531f --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java @@ -0,0 +1,60 @@ +/* + * 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 + * + * http://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. + */ + +/* + * 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 + * + * http://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 static org.assertj.core.api.Assertions.assertThat; +import example.aot.UserRepository; +import org.junit.jupiter.api.Test; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.core.test.tools.TestCompiler; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public class MongoRepositoryContributorTests { + + @Test + public void testCompile() { + + TestMongoAotRepositoryContext aotContext = new TestMongoAotRepositoryContext(UserRepository.class, null); + TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class); + + new MongoRepositoryContributor(aotContext).contribute(generationContext); + generationContext.writeGeneratedContent(); + + TestCompiler.forSystem().with(generationContext).compile(compiled -> { + assertThat(compiled.getAllCompiledClasses()).map(Class::getName).contains("example.aot.UserRepositoryImpl__Aot"); + }); + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/StubRepositoryInformation.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/StubRepositoryInformation.java new file mode 100644 index 0000000000..52d609be63 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/StubRepositoryInformation.java @@ -0,0 +1,144 @@ +/* + * 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 + * + * http://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. + */ + +/* + * 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 + * + * http://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.lang.reflect.Method; +import java.util.Set; + +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.Streamable; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ + +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 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) { + return false; + } + + @Override + public Streamable getQueryMethods() { + return null; + } + + @Override + public Class getRepositoryBaseClass() { + return SimpleMongoRepository.class; + } + + @Override + public Method getTargetClassMethod(Method method) { + return null; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/TestMongoAotRepositoryContext.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/TestMongoAotRepositoryContext.java new file mode 100644 index 0000000000..e0efcd434c --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/TestMongoAotRepositoryContext.java @@ -0,0 +1,121 @@ +/* + * 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 + * + * http://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. + */ + +/* + * 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 + * + * http://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.io.IOException; +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Set; + +import org.springframework.core.test.tools.ClassFile; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public class TestMongoAotRepositoryContext implements AotRepositoryContext { + + private final StubRepositoryInformation repositoryInformation; + + 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 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())); + } + } +} diff --git a/spring-data-mongodb/src/test/resources/logback.xml b/spring-data-mongodb/src/test/resources/logback.xml index 64550c957c..9a65ce79b8 100644 --- a/spring-data-mongodb/src/test/resources/logback.xml +++ b/spring-data-mongodb/src/test/resources/logback.xml @@ -18,6 +18,7 @@ + From 3b31cb6632647487edd1d12c3890fde29d6269b8 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 10 Jan 2025 10:01:14 +0100 Subject: [PATCH 03/13] hacking to get part tree to run --- .../generated/MongoRepositoryContributor.java | 214 +++++++++-- .../data/mongodb/core/query/Criteria.java | 15 +- .../core/query/CriteriaDefinition.java | 17 + .../data/mongodb/core/query/Query.java | 358 +++++++++++++++++- .../repository/query/MongoQueryCreator.java | 2 +- .../data/mongodb/util/SpringJsonWriter.java | 287 ++++++++++++++ .../test/java/example/aot/UserRepository.java | 4 + 7 files changed, 861 insertions(+), 36 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/SpringJsonWriter.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java index 216f67e6c5..96c8fc2795 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java @@ -17,22 +17,53 @@ import java.lang.reflect.Parameter; import java.util.Arrays; +import java.util.Iterator; +import java.util.List; import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.apache.commons.logging.Log; +import org.bson.conversions.Bson; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Sort; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.Point; import org.springframework.data.mongodb.BindableMongoExpression; import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; +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.mapping.MongoSimpleTypes; import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder; +import org.springframework.data.mongodb.core.query.TextCriteria; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.repository.Query; +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.aot.generate.AotRepositoryConstructorBuilder; import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder; +import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder.MethodGenerationMetadata; 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.query.parser.PartTree; +import org.springframework.data.util.TypeInformation; +import org.springframework.javapoet.MethodSpec.Builder; import org.springframework.javapoet.TypeName; +import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; +import com.mongodb.DBRef; + /** * @author Christoph Strobl * @since 2025/01 @@ -54,39 +85,160 @@ protected void customizeDerivedMethod(AotRepositoryMethodBuilder methodBuilder) methodBuilder.customize((repositoryInformation, metadata, body) -> { Query query = AnnotatedElementUtils.findMergedAnnotation(metadata.getRepositoryMethod(), Query.class); if (query != null) { + userAnnotatedQuery(repositoryInformation, metadata, body, query); + } else { + + + ; + + MongoMappingContext mongoMappingContext = new MongoMappingContext(); + mongoMappingContext.setSimpleTypeHolder(MongoCustomConversions.create((cfg) -> + cfg.useNativeDriverJavaTimeCodecs()).getSimpleTypeHolder()); + mongoMappingContext.setAutoIndexCreation(false); + mongoMappingContext.afterPropertiesSet(); + + PartTree partTree = new PartTree(metadata.getRepositoryMethod().getName(), + repositoryInformation.getDomainType()); + MongoQueryCreator queryCreator = new MongoQueryCreator(partTree, + new ConvertingParameterAccessor(new MongoWriter() { + @Nullable + @Override + public Object convertToMongoType(@Nullable Object obj, @Nullable TypeInformation typeInformation) { + return "?0"; + } + + @Override + public DBRef toDBRef(Object object, @Nullable MongoPersistentProperty referringProperty) { + return null; + } + + @Override + public void write(Object source, Bson sink) { + + } + }, new MongoParameterAccessor() { + @Override + public Range getDistanceRange() { + return null; + } + + @Nullable + @Override + public Point getGeoNearLocation() { + return null; + } + + @Nullable + @Override + public TextCriteria getFullText() { + return null; + } + + @Nullable + @Override + public Collation getCollation() { + return null; + } + + @Override + public Object[] getValues() { + + if(metadata.getRepositoryMethod().getParameterCount() == 0) { + return new Object[]{}; + } + return IntStream.range(0, metadata.getRepositoryMethod().getParameterCount()).mapToObj(it -> new Placeholder("?"+it)).toArray(); + } + + @Nullable + @Override + public UpdateDefinition getUpdate() { + return null; + } + + @Nullable + @Override + public ScrollPosition getScrollPosition() { + return null; + } + + @Override + public Pageable getPageable() { + return null; + } - String mongoOpsRef = metadata.fieldNameOf(MongoOperations.class); - String arguments = StringUtils - .collectionToCommaDelimitedString(Arrays.stream(metadata.getRepositoryMethod().getParameters()) - .map(Parameter::getName).collect(Collectors.toList())); - - body.beginControlFlow("if($L.isDebugEnabled())", metadata.fieldNameOf(Log.class)); - body.addStatement("$L.debug(\"invoking generated [$L] method\")", metadata.fieldNameOf(Log.class), - metadata.getRepositoryMethod().getName()); - body.endControlFlow(); - - body.addStatement("$T filter = new $T($S, $L.getConverter(), new $T[]{ $L })", BindableMongoExpression.class, - BindableMongoExpression.class, query.value(), mongoOpsRef, Object.class, arguments); - body.addStatement("$T query = new $T(filter.toDocument())", - org.springframework.data.mongodb.core.query.Query.class, BasicQuery.class); - - if (metadata.getActualReturnType() != null && ObjectUtils - .nullSafeEquals(TypeName.get(repositoryInformation.getDomainType()), metadata.getActualReturnType())) { - body.addStatement(""" - return $L.query($T.class) - .matching(query) - .all()""", mongoOpsRef, repositoryInformation.getDomainType()); - } else { - - body.addStatement(""" - return $L.query($T.class) - .as($T.class) - .matching(query) - .all()""", mongoOpsRef, repositoryInformation.getDomainType(), - metadata.getActualReturnType() != null ? metadata.getActualReturnType() - : repositoryInformation.getDomainType()); - } + @Override + public Sort getSort() { + return null; + } + + @Nullable + @Override + public Class findDynamicProjection() { + return null; + } + + @Nullable + @Override + public Object getBindableValue(int index) { + return "?" + index; + } + + @Override + public boolean hasBindableNullValue() { + return false; + } + + @Override + public Iterator iterator() { + return Arrays.stream(getValues()).iterator(); + } + }), mongoMappingContext); + + writeStringQuery(repositoryInformation, metadata, body, queryCreator.createQuery().toJson()); } }); } + + private static void writeStringQuery(RepositoryInformation repositoryInformation, MethodGenerationMetadata metadata, + Builder body, String query) { + + String mongoOpsRef = metadata.fieldNameOf(MongoOperations.class); + String arguments = StringUtils.collectionToCommaDelimitedString(Arrays + .stream(metadata.getRepositoryMethod().getParameters()).map(Parameter::getName).collect(Collectors.toList())); + + body.beginControlFlow("if($L.isDebugEnabled())", metadata.fieldNameOf(Log.class)); + body.addStatement("$L.debug(\"invoking generated [$L] method\")", metadata.fieldNameOf(Log.class), + metadata.getRepositoryMethod().getName()); + body.endControlFlow(); + + body.addStatement("$T filter = new $T($S, $L.getConverter(), new $T[]{ $L })", BindableMongoExpression.class, + BindableMongoExpression.class, query, mongoOpsRef, Object.class, arguments); + body.addStatement("$T query = new $T(filter.toDocument())", org.springframework.data.mongodb.core.query.Query.class, + BasicQuery.class); + + boolean isCollectionType = TypeInformation.fromReturnTypeOf(metadata.getRepositoryMethod()).isCollectionLike(); + String terminatingMethod = isCollectionType ? "all()" : "oneValue()"; + + if (metadata.getActualReturnType() != null && ObjectUtils + .nullSafeEquals(TypeName.get(repositoryInformation.getDomainType()), metadata.getActualReturnType())) { + body.addStatement(""" + return $L.query($T.class) + .matching(query) + .$L""", mongoOpsRef, repositoryInformation.getDomainType(), terminatingMethod); + } else { + + body.addStatement(""" + return $L.query($T.class) + .as($T.class) + .matching(query) + .$L""", mongoOpsRef, repositoryInformation.getDomainType(), + metadata.getActualReturnType() != null ? metadata.getActualReturnType() + : repositoryInformation.getDomainType(), terminatingMethod); + } + } + + private static void userAnnotatedQuery(RepositoryInformation repositoryInformation, MethodGenerationMetadata metadata, + Builder body, Query query) { + writeStringQuery(repositoryInformation, metadata, body, query.value()); + } } 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..b9c57dce82 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 @@ -29,9 +29,21 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; +import com.mongodb.MongoClientSettings; +import org.bson.BsonReader; import org.bson.BsonRegularExpression; import org.bson.BsonType; +import org.bson.BsonWriter; import org.bson.Document; +import org.bson.codecs.Codec; +import org.bson.codecs.DecoderContext; +import org.bson.codecs.DocumentCodec; +import org.bson.codecs.DocumentCodecProvider; +import org.bson.codecs.Encoder; +import org.bson.codecs.EncoderContext; +import org.bson.codecs.configuration.CodecProvider; +import org.bson.codecs.configuration.CodecRegistries; +import org.bson.codecs.configuration.CodecRegistry; import org.bson.types.Binary; import org.springframework.data.domain.Example; import org.springframework.data.geo.Circle; @@ -900,7 +912,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; 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..f40ecd6b4b 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 @@ -40,4 +40,21 @@ public interface CriteriaDefinition { @Nullable String getKey(); + class Placeholder { + + Object value; + + public Placeholder(Object value) { + this.value = value; + } + + public Object getValue() { + return value; + } + + @Override + public String toString() { + return getValue().toString(); + } + } } 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..fd43c02e7f 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,21 +15,36 @@ */ 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; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.Set; - +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +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.Document; +import org.bson.codecs.EncoderContext; +import org.bson.codecs.PatternCodec; +import org.bson.types.Decimal128; +import org.bson.types.ObjectId; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.Limit; import org.springframework.data.domain.OffsetScrollPosition; @@ -40,10 +55,12 @@ import org.springframework.data.mongodb.InvalidMongoDbApiUsageException; import org.springframework.data.mongodb.core.ReadConcernAware; import org.springframework.data.mongodb.core.ReadPreferenceAware; +import org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder; import org.springframework.data.mongodb.core.query.Meta.CursorOption; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import com.mongodb.ReadConcern; import com.mongodb.ReadPreference; @@ -440,6 +457,341 @@ public Document getQueryObject() { return document; } + private String toJson(Document document) { + StringBuilder json = new StringBuilder("{ "); + if (!document.isEmpty()) { + for (Entry entry : document.entrySet()) { + json.append("%s : %s,".formatted(entry.getKey(), toJsonValue(entry.getValue()))); + } + + json.deleteCharAt(json.length() - 1); + } + return json + " }"; + + } + + private String toJsonValue(Object source) { + if (source == null) { + return "null"; + } + + if (source instanceof Document nested) { + return toJson(nested); + } + if (source instanceof Placeholder p) { + return p.getValue().toString(); + } + if (source instanceof String s) { + return "'%s'".formatted(s); + } + if (source instanceof Pattern pattern) { + + return "{ $regex : /%s/ }".formatted(pattern.pattern()); + +// StringBuffer buffer = new StringBuffer(); +// BsonWriter writer = new BsonWriter() { +// @Override +// public void flush() { +// +// } +// +// @Override +// public void writeBinaryData(BsonBinary binary) { +// +// } +// +// @Override +// public void writeBinaryData(String name, BsonBinary binary) { +// +// } +// +// @Override +// public void writeBoolean(boolean value) { +// +// } +// +// @Override +// public void writeBoolean(String name, boolean value) { +// +// } +// +// @Override +// public void writeDateTime(long value) { +// +// } +// +// @Override +// public void writeDateTime(String name, long value) { +// +// } +// +// @Override +// public void writeDBPointer(BsonDbPointer value) { +// +// } +// +// @Override +// public void writeDBPointer(String name, BsonDbPointer value) { +// +// } +// +// @Override +// public void writeDouble(double value) { +// +// } +// +// @Override +// public void writeDouble(String name, double value) { +// +// } +// +// @Override +// public void writeEndArray() { +// +// } +// +// @Override +// public void writeEndDocument() { +// +// } +// +// @Override +// public void writeInt32(int value) { +// +// } +// +// @Override +// public void writeInt32(String name, int value) { +// +// } +// +// @Override +// public void writeInt64(long value) { +// +// } +// +// @Override +// public void writeInt64(String name, long value) { +// +// } +// +// @Override +// public void writeDecimal128(Decimal128 value) { +// +// } +// +// @Override +// public void writeDecimal128(String name, Decimal128 value) { +// +// } +// +// @Override +// public void writeJavaScript(String code) { +// +// } +// +// @Override +// public void writeJavaScript(String name, String code) { +// +// } +// +// @Override +// public void writeJavaScriptWithScope(String code) { +// +// } +// +// @Override +// public void writeJavaScriptWithScope(String name, String code) { +// +// } +// +// @Override +// public void writeMaxKey() { +// +// } +// +// @Override +// public void writeMaxKey(String name) { +// +// } +// +// @Override +// public void writeMinKey() { +// +// } +// +// @Override +// public void writeMinKey(String name) { +// +// } +// +// @Override +// public void writeName(String name) { +// buffer.append(name); +// buffer.append(":"); +// } +// +// @Override +// public void writeNull() { +// +// } +// +// @Override +// public void writeNull(String name) { +// +// } +// +// @Override +// public void writeObjectId(ObjectId objectId) { +// +// } +// +// @Override +// public void writeObjectId(String name, ObjectId objectId) { +// +// } +// +// @Override +// public void writeRegularExpression(BsonRegularExpression regularExpression) { +// buffer.append("{"); +// buffer.append("$regex"); +// buffer.append(":"); +// buffer.append(regularExpression.getPattern()); +// if (!regularExpression.getOptions().isBlank()) { +// buffer.append(","); +// buffer.append("$options"); +// buffer.append(":"); +// buffer.append("'" + regularExpression.getOptions() + "'"); +// } +// buffer.append("}"); +// } +// +// @Override +// public void writeRegularExpression(String name, BsonRegularExpression regularExpression) { +// +// } +// +// @Override +// public void writeStartArray() { +// +// } +// +// @Override +// public void writeStartArray(String name) { +// +// } +// +// @Override +// public void writeStartDocument() { +// +// } +// +// @Override +// public void writeStartDocument(String name) { +// +// } +// +// @Override +// public void writeString(String value) { +// +// } +// +// @Override +// public void writeString(String name, String value) { +// +// } +// +// @Override +// public void writeSymbol(String value) { +// +// } +// +// @Override +// public void writeSymbol(String name, String value) { +// +// } +// +// @Override +// public void writeTimestamp(BsonTimestamp value) { +// +// } +// +// @Override +// public void writeTimestamp(String name, BsonTimestamp value) { +// +// } +// +// @Override +// public void writeUndefined() { +// +// } +// +// @Override +// public void writeUndefined(String name) { +// +// } +// +// @Override +// public void pipe(BsonReader reader) { +// +// } +// }; +// new PatternCodec().encode(writer, pattern, EncoderContext.builder().build()); +// return buffer.toString(); + } + if (source instanceof Collection collection) { + String value = "["; + value += StringUtils + .collectionToCommaDelimitedString(collection.stream().map(this::toJsonValue).collect(Collectors.toList())); + return value + "]"; + } + if (source instanceof Map map) { + return toJson(new Document((Map) map)); + } + + return source.toString(); + } + + public String toJson() { + + return toJson(getQueryObject()); + + // CodecRegistry placeholderRegistry = CodecRegistries.fromCodecs(new Codec() { + // @Override + // public Placeholder decode(BsonReader reader, DecoderContext decoderContext) { + // return null; + // } + // + // @Override + // public void encode(BsonWriter writer, Placeholder value, EncoderContext encoderContext) { + // + // if(writer instanceof JsonWriter jw) { + // try { + // jw.getWriter().write(value.getValue().toString()); + // jw.flush(); + // } catch (IOException e) { + // throw new RuntimeException(e); + // } + // } + //// Method add = ReflectionUtils.findMethod(StrictCharacterStreamJsonWriter.class, "write", String.class); + //// ReflectionUtils.makeAccessible(add); + //// ReflectionUtils.invokeMethod(add, writer, ); + // } + // + // @Override + // public Class getEncoderClass() { + // return Placeholder.class; + // } + // }); + + // new StrictCharacterStreamJsonWriter() + // + // CodecRegistry combinedRegistry = CodecRegistries.fromRegistries(MongoClientSettings.getDefaultCodecRegistry(), + // placeholderRegistry); + // + // return getQueryObject().toJson(JsonWriterSettings.builder().build(), combinedRegistry.get(Document.class)); + } + /** * @return the field {@link Document}. */ 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..3e94208166 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 @@ -65,7 +65,7 @@ * @author Christoph Strobl * @author Edward Prentice */ -class MongoQueryCreator extends AbstractQueryCreator { +public class MongoQueryCreator extends AbstractQueryCreator { private static final Log LOG = LogFactory.getLog(MongoQueryCreator.class); 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..650bf3ebda --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/SpringJsonWriter.java @@ -0,0 +1,287 @@ +/* + * 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 + * + * http://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. + */ + +/* + * 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 + * + * http://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 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; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public class SpringJsonWriter implements BsonWriter { + + @Override + public void flush() { + + } + + @Override + public void writeBinaryData(BsonBinary binary) { + + } + + @Override + public void writeBinaryData(String name, BsonBinary binary) { + + } + + @Override + public void writeBoolean(boolean value) { + + } + + @Override + public void writeBoolean(String name, boolean value) { + + } + + @Override + public void writeDateTime(long value) { + + } + + @Override + public void writeDateTime(String name, long value) { + + } + + @Override + public void writeDBPointer(BsonDbPointer value) { + + } + + @Override + public void writeDBPointer(String name, BsonDbPointer value) { + + } + + @Override + public void writeDouble(double value) { + + } + + @Override + public void writeDouble(String name, double value) { + + } + + @Override + public void writeEndArray() { + + } + + @Override + public void writeEndDocument() { + + } + + @Override + public void writeInt32(int value) { + + } + + @Override + public void writeInt32(String name, int value) { + + } + + @Override + public void writeInt64(long value) { + + } + + @Override + public void writeInt64(String name, long value) { + + } + + @Override + public void writeDecimal128(Decimal128 value) { + + } + + @Override + public void writeDecimal128(String name, Decimal128 value) { + + } + + @Override + public void writeJavaScript(String code) { + + } + + @Override + public void writeJavaScript(String name, String code) { + + } + + @Override + public void writeJavaScriptWithScope(String code) { + + } + + @Override + public void writeJavaScriptWithScope(String name, String code) { + + } + + @Override + public void writeMaxKey() { + + } + + @Override + public void writeMaxKey(String name) { + + } + + @Override + public void writeMinKey() { + + } + + @Override + public void writeMinKey(String name) { + + } + + @Override + public void writeName(String name) { + + } + + @Override + public void writeNull() { + + } + + @Override + public void writeNull(String name) { + + } + + @Override + public void writeObjectId(ObjectId objectId) { + + } + + @Override + public void writeObjectId(String name, ObjectId objectId) { + + } + + @Override + public void writeRegularExpression(BsonRegularExpression regularExpression) { + + } + + @Override + public void writeRegularExpression(String name, BsonRegularExpression regularExpression) { + + } + + @Override + public void writeStartArray() { + + } + + @Override + public void writeStartArray(String name) { + + } + + @Override + public void writeStartDocument() { + + } + + @Override + public void writeStartDocument(String name) { + + } + + @Override + public void writeString(String value) { + + } + + @Override + public void writeString(String name, String value) { + + } + + @Override + public void writeSymbol(String value) { + + } + + @Override + public void writeSymbol(String name, String value) { + + } + + @Override + public void writeTimestamp(BsonTimestamp value) { + + } + + @Override + public void writeTimestamp(String name, BsonTimestamp value) { + + } + + @Override + public void writeUndefined() { + + } + + @Override + public void writeUndefined(String name) { + + } + + @Override + public void pipe(BsonReader reader) { + + } + + public void writePlaceholder(String placeholder) { + + } +} diff --git a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java index bda2e142f9..441cd3da83 100644 --- a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java +++ b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java @@ -29,4 +29,8 @@ public interface UserRepository extends CrudRepository { @Query("{ 'username' : '?0' }") List findAllByAnnotatedQueryWithParameter(String username); + + User findByUsername(String username); + + List findUserByLastnameLike(String lastname); } From b046cb7fdf5829a328711d2c9ca022b667f22396 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 10 Jan 2025 12:23:11 +0100 Subject: [PATCH 04/13] Use dedicated json writer to render query --- .../generated/MongoRepositoryContributor.java | 38 +- .../data/mongodb/core/query/Query.java | 353 +------------ .../data/mongodb/util/BsonUtils.java | 66 ++- .../data/mongodb/util/SpringJsonWriter.java | 287 ----------- .../mongodb/util/json/SpringJsonWriter.java | 478 ++++++++++++++++++ .../util/json/SpringJsonWriterUnitTests.java | 159 ++++++ 6 files changed, 723 insertions(+), 658 deletions(-) delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/SpringJsonWriter.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/SpringJsonWriter.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/SpringJsonWriterUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java index 96c8fc2795..ea17ffb774 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java @@ -18,7 +18,6 @@ import java.lang.reflect.Parameter; import java.util.Arrays; import java.util.Iterator; -import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -34,11 +33,9 @@ import org.springframework.data.mongodb.BindableMongoExpression; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.convert.MongoCustomConversions; -import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; 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.mapping.MongoSimpleTypes; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder; @@ -48,6 +45,7 @@ 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.mongodb.util.BsonUtils; import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder; import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder; import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder.MethodGenerationMetadata; @@ -88,12 +86,11 @@ protected void customizeDerivedMethod(AotRepositoryMethodBuilder methodBuilder) userAnnotatedQuery(repositoryInformation, metadata, body, query); } else { - ; MongoMappingContext mongoMappingContext = new MongoMappingContext(); - mongoMappingContext.setSimpleTypeHolder(MongoCustomConversions.create((cfg) -> - cfg.useNativeDriverJavaTimeCodecs()).getSimpleTypeHolder()); + mongoMappingContext.setSimpleTypeHolder( + MongoCustomConversions.create((cfg) -> cfg.useNativeDriverJavaTimeCodecs()).getSimpleTypeHolder()); mongoMappingContext.setAutoIndexCreation(false); mongoMappingContext.afterPropertiesSet(); @@ -143,10 +140,11 @@ public Collation getCollation() { @Override public Object[] getValues() { - if(metadata.getRepositoryMethod().getParameterCount() == 0) { - return new Object[]{}; + if (metadata.getRepositoryMethod().getParameterCount() == 0) { + return new Object[] {}; } - return IntStream.range(0, metadata.getRepositoryMethod().getParameterCount()).mapToObj(it -> new Placeholder("?"+it)).toArray(); + return IntStream.range(0, metadata.getRepositoryMethod().getParameterCount()) + .mapToObj(it -> new Placeholder("?" + it)).toArray(); } @Nullable @@ -194,33 +192,36 @@ public Iterator iterator() { } }), mongoMappingContext); - writeStringQuery(repositoryInformation, metadata, body, queryCreator.createQuery().toJson()); + org.springframework.data.mongodb.core.query.Query partTreeQuery = queryCreator.createQuery(); + StringBuffer buffer = new StringBuffer(); + BsonUtils.writeJson(partTreeQuery.getQueryObject()).to(buffer); + writeStringQuery(repositoryInformation, metadata, body, buffer.toString()); } }); } private static void writeStringQuery(RepositoryInformation repositoryInformation, MethodGenerationMetadata metadata, - Builder body, String query) { + Builder body, String query) { String mongoOpsRef = metadata.fieldNameOf(MongoOperations.class); String arguments = StringUtils.collectionToCommaDelimitedString(Arrays - .stream(metadata.getRepositoryMethod().getParameters()).map(Parameter::getName).collect(Collectors.toList())); + .stream(metadata.getRepositoryMethod().getParameters()).map(Parameter::getName).collect(Collectors.toList())); body.beginControlFlow("if($L.isDebugEnabled())", metadata.fieldNameOf(Log.class)); body.addStatement("$L.debug(\"invoking generated [$L] method\")", metadata.fieldNameOf(Log.class), - metadata.getRepositoryMethod().getName()); + metadata.getRepositoryMethod().getName()); body.endControlFlow(); body.addStatement("$T filter = new $T($S, $L.getConverter(), new $T[]{ $L })", BindableMongoExpression.class, - BindableMongoExpression.class, query, mongoOpsRef, Object.class, arguments); + BindableMongoExpression.class, query, mongoOpsRef, Object.class, arguments); body.addStatement("$T query = new $T(filter.toDocument())", org.springframework.data.mongodb.core.query.Query.class, - BasicQuery.class); + BasicQuery.class); boolean isCollectionType = TypeInformation.fromReturnTypeOf(metadata.getRepositoryMethod()).isCollectionLike(); String terminatingMethod = isCollectionType ? "all()" : "oneValue()"; if (metadata.getActualReturnType() != null && ObjectUtils - .nullSafeEquals(TypeName.get(repositoryInformation.getDomainType()), metadata.getActualReturnType())) { + .nullSafeEquals(TypeName.get(repositoryInformation.getDomainType()), metadata.getActualReturnType())) { body.addStatement(""" return $L.query($T.class) .matching(query) @@ -232,8 +233,9 @@ private static void writeStringQuery(RepositoryInformation repositoryInformation .as($T.class) .matching(query) .$L""", mongoOpsRef, repositoryInformation.getDomainType(), - metadata.getActualReturnType() != null ? metadata.getActualReturnType() - : repositoryInformation.getDomainType(), terminatingMethod); + metadata.getActualReturnType() != null ? metadata.getActualReturnType() + : repositoryInformation.getDomainType(), + terminatingMethod); } } 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 fd43c02e7f..85cc9c5c1a 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 @@ -22,29 +22,15 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Optional; import java.util.Set; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -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.Document; -import org.bson.codecs.EncoderContext; -import org.bson.codecs.PatternCodec; -import org.bson.types.Decimal128; -import org.bson.types.ObjectId; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.Limit; import org.springframework.data.domain.OffsetScrollPosition; @@ -55,12 +41,10 @@ import org.springframework.data.mongodb.InvalidMongoDbApiUsageException; import org.springframework.data.mongodb.core.ReadConcernAware; import org.springframework.data.mongodb.core.ReadPreferenceAware; -import org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder; import org.springframework.data.mongodb.core.query.Meta.CursorOption; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; import com.mongodb.ReadConcern; import com.mongodb.ReadPreference; @@ -457,341 +441,6 @@ public Document getQueryObject() { return document; } - private String toJson(Document document) { - StringBuilder json = new StringBuilder("{ "); - if (!document.isEmpty()) { - for (Entry entry : document.entrySet()) { - json.append("%s : %s,".formatted(entry.getKey(), toJsonValue(entry.getValue()))); - } - - json.deleteCharAt(json.length() - 1); - } - return json + " }"; - - } - - private String toJsonValue(Object source) { - if (source == null) { - return "null"; - } - - if (source instanceof Document nested) { - return toJson(nested); - } - if (source instanceof Placeholder p) { - return p.getValue().toString(); - } - if (source instanceof String s) { - return "'%s'".formatted(s); - } - if (source instanceof Pattern pattern) { - - return "{ $regex : /%s/ }".formatted(pattern.pattern()); - -// StringBuffer buffer = new StringBuffer(); -// BsonWriter writer = new BsonWriter() { -// @Override -// public void flush() { -// -// } -// -// @Override -// public void writeBinaryData(BsonBinary binary) { -// -// } -// -// @Override -// public void writeBinaryData(String name, BsonBinary binary) { -// -// } -// -// @Override -// public void writeBoolean(boolean value) { -// -// } -// -// @Override -// public void writeBoolean(String name, boolean value) { -// -// } -// -// @Override -// public void writeDateTime(long value) { -// -// } -// -// @Override -// public void writeDateTime(String name, long value) { -// -// } -// -// @Override -// public void writeDBPointer(BsonDbPointer value) { -// -// } -// -// @Override -// public void writeDBPointer(String name, BsonDbPointer value) { -// -// } -// -// @Override -// public void writeDouble(double value) { -// -// } -// -// @Override -// public void writeDouble(String name, double value) { -// -// } -// -// @Override -// public void writeEndArray() { -// -// } -// -// @Override -// public void writeEndDocument() { -// -// } -// -// @Override -// public void writeInt32(int value) { -// -// } -// -// @Override -// public void writeInt32(String name, int value) { -// -// } -// -// @Override -// public void writeInt64(long value) { -// -// } -// -// @Override -// public void writeInt64(String name, long value) { -// -// } -// -// @Override -// public void writeDecimal128(Decimal128 value) { -// -// } -// -// @Override -// public void writeDecimal128(String name, Decimal128 value) { -// -// } -// -// @Override -// public void writeJavaScript(String code) { -// -// } -// -// @Override -// public void writeJavaScript(String name, String code) { -// -// } -// -// @Override -// public void writeJavaScriptWithScope(String code) { -// -// } -// -// @Override -// public void writeJavaScriptWithScope(String name, String code) { -// -// } -// -// @Override -// public void writeMaxKey() { -// -// } -// -// @Override -// public void writeMaxKey(String name) { -// -// } -// -// @Override -// public void writeMinKey() { -// -// } -// -// @Override -// public void writeMinKey(String name) { -// -// } -// -// @Override -// public void writeName(String name) { -// buffer.append(name); -// buffer.append(":"); -// } -// -// @Override -// public void writeNull() { -// -// } -// -// @Override -// public void writeNull(String name) { -// -// } -// -// @Override -// public void writeObjectId(ObjectId objectId) { -// -// } -// -// @Override -// public void writeObjectId(String name, ObjectId objectId) { -// -// } -// -// @Override -// public void writeRegularExpression(BsonRegularExpression regularExpression) { -// buffer.append("{"); -// buffer.append("$regex"); -// buffer.append(":"); -// buffer.append(regularExpression.getPattern()); -// if (!regularExpression.getOptions().isBlank()) { -// buffer.append(","); -// buffer.append("$options"); -// buffer.append(":"); -// buffer.append("'" + regularExpression.getOptions() + "'"); -// } -// buffer.append("}"); -// } -// -// @Override -// public void writeRegularExpression(String name, BsonRegularExpression regularExpression) { -// -// } -// -// @Override -// public void writeStartArray() { -// -// } -// -// @Override -// public void writeStartArray(String name) { -// -// } -// -// @Override -// public void writeStartDocument() { -// -// } -// -// @Override -// public void writeStartDocument(String name) { -// -// } -// -// @Override -// public void writeString(String value) { -// -// } -// -// @Override -// public void writeString(String name, String value) { -// -// } -// -// @Override -// public void writeSymbol(String value) { -// -// } -// -// @Override -// public void writeSymbol(String name, String value) { -// -// } -// -// @Override -// public void writeTimestamp(BsonTimestamp value) { -// -// } -// -// @Override -// public void writeTimestamp(String name, BsonTimestamp value) { -// -// } -// -// @Override -// public void writeUndefined() { -// -// } -// -// @Override -// public void writeUndefined(String name) { -// -// } -// -// @Override -// public void pipe(BsonReader reader) { -// -// } -// }; -// new PatternCodec().encode(writer, pattern, EncoderContext.builder().build()); -// return buffer.toString(); - } - if (source instanceof Collection collection) { - String value = "["; - value += StringUtils - .collectionToCommaDelimitedString(collection.stream().map(this::toJsonValue).collect(Collectors.toList())); - return value + "]"; - } - if (source instanceof Map map) { - return toJson(new Document((Map) map)); - } - - return source.toString(); - } - - public String toJson() { - - return toJson(getQueryObject()); - - // CodecRegistry placeholderRegistry = CodecRegistries.fromCodecs(new Codec() { - // @Override - // public Placeholder decode(BsonReader reader, DecoderContext decoderContext) { - // return null; - // } - // - // @Override - // public void encode(BsonWriter writer, Placeholder value, EncoderContext encoderContext) { - // - // if(writer instanceof JsonWriter jw) { - // try { - // jw.getWriter().write(value.getValue().toString()); - // jw.flush(); - // } catch (IOException e) { - // throw new RuntimeException(e); - // } - // } - //// Method add = ReflectionUtils.findMethod(StrictCharacterStreamJsonWriter.class, "write", String.class); - //// ReflectionUtils.makeAccessible(add); - //// ReflectionUtils.invokeMethod(add, writer, ); - // } - // - // @Override - // public Class getEncoderClass() { - // return Placeholder.class; - // } - // }); - - // new StrictCharacterStreamJsonWriter() - // - // CodecRegistry combinedRegistry = CodecRegistries.fromRegistries(MongoClientSettings.getDefaultCodecRegistry(), - // placeholderRegistry); - // - // return getQueryObject().toJson(JsonWriterSettings.builder().build(), combinedRegistry.get(Document.class)); - } - /** * @return the field {@link Document}. */ 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 7a70ac0445..a3de5ba5c9 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,11 +29,37 @@ 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.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; @@ -44,6 +70,8 @@ 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.data.mongodb.core.query.CriteriaDefinition.Placeholder; +import org.springframework.data.mongodb.util.json.SpringJsonWriter; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -72,6 +100,9 @@ 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) { @@ -737,6 +768,17 @@ public static Document mapEntries(Document source, Function { + SpringJsonWriter writer = new SpringJsonWriter(sink); + JSON_CODEC_REGISTRY.get(Document.class).encode(writer, document, EncoderContext.builder().build()); + }; + } + + public interface JsonWriter { + void to(StringBuffer sink); + } + @Nullable private static String toJson(@Nullable Object value) { @@ -949,4 +991,26 @@ public void flush() { values.clear(); } } + + 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/SpringJsonWriter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/SpringJsonWriter.java deleted file mode 100644 index 650bf3ebda..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/SpringJsonWriter.java +++ /dev/null @@ -1,287 +0,0 @@ -/* - * 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 - * - * http://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. - */ - -/* - * 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 - * - * http://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 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; - -/** - * @author Christoph Strobl - * @since 2025/01 - */ -public class SpringJsonWriter implements BsonWriter { - - @Override - public void flush() { - - } - - @Override - public void writeBinaryData(BsonBinary binary) { - - } - - @Override - public void writeBinaryData(String name, BsonBinary binary) { - - } - - @Override - public void writeBoolean(boolean value) { - - } - - @Override - public void writeBoolean(String name, boolean value) { - - } - - @Override - public void writeDateTime(long value) { - - } - - @Override - public void writeDateTime(String name, long value) { - - } - - @Override - public void writeDBPointer(BsonDbPointer value) { - - } - - @Override - public void writeDBPointer(String name, BsonDbPointer value) { - - } - - @Override - public void writeDouble(double value) { - - } - - @Override - public void writeDouble(String name, double value) { - - } - - @Override - public void writeEndArray() { - - } - - @Override - public void writeEndDocument() { - - } - - @Override - public void writeInt32(int value) { - - } - - @Override - public void writeInt32(String name, int value) { - - } - - @Override - public void writeInt64(long value) { - - } - - @Override - public void writeInt64(String name, long value) { - - } - - @Override - public void writeDecimal128(Decimal128 value) { - - } - - @Override - public void writeDecimal128(String name, Decimal128 value) { - - } - - @Override - public void writeJavaScript(String code) { - - } - - @Override - public void writeJavaScript(String name, String code) { - - } - - @Override - public void writeJavaScriptWithScope(String code) { - - } - - @Override - public void writeJavaScriptWithScope(String name, String code) { - - } - - @Override - public void writeMaxKey() { - - } - - @Override - public void writeMaxKey(String name) { - - } - - @Override - public void writeMinKey() { - - } - - @Override - public void writeMinKey(String name) { - - } - - @Override - public void writeName(String name) { - - } - - @Override - public void writeNull() { - - } - - @Override - public void writeNull(String name) { - - } - - @Override - public void writeObjectId(ObjectId objectId) { - - } - - @Override - public void writeObjectId(String name, ObjectId objectId) { - - } - - @Override - public void writeRegularExpression(BsonRegularExpression regularExpression) { - - } - - @Override - public void writeRegularExpression(String name, BsonRegularExpression regularExpression) { - - } - - @Override - public void writeStartArray() { - - } - - @Override - public void writeStartArray(String name) { - - } - - @Override - public void writeStartDocument() { - - } - - @Override - public void writeStartDocument(String name) { - - } - - @Override - public void writeString(String value) { - - } - - @Override - public void writeString(String name, String value) { - - } - - @Override - public void writeSymbol(String value) { - - } - - @Override - public void writeSymbol(String name, String value) { - - } - - @Override - public void writeTimestamp(BsonTimestamp value) { - - } - - @Override - public void writeTimestamp(String name, BsonTimestamp value) { - - } - - @Override - public void writeUndefined() { - - } - - @Override - public void writeUndefined(String name) { - - } - - @Override - public void pipe(BsonReader reader) { - - } - - public void writePlaceholder(String placeholder) { - - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/SpringJsonWriter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/SpringJsonWriter.java new file mode 100644 index 0000000000..fdbf5ae4d6 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/SpringJsonWriter.java @@ -0,0 +1,478 @@ +/* + * 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.json; + +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.springframework.util.StringUtils; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public 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"); + buffer.append(value); + 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"); + buffer.append(value); + writeEndDocument(); + } + + @Override + public void writeInt32(String name, int value) { + + writeName(name); + writeInt32(value); + } + + @Override + public void writeInt64(long value) { + + writeStartDocument(); + writeName("$numberLong"); + buffer.append(value); + writeEndDocument(); + } + + @Override + public void writeInt64(String name, long value) { + + writeName(name); + writeInt64(value); + } + + @Override + public void writeDecimal128(Decimal128 value) { + + // { "$numberDecimal": "" } + writeStartDocument(); + writeName("$numberDecimal"); + write(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) { + + } + + 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/test/java/org/springframework/data/mongodb/util/json/SpringJsonWriterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/SpringJsonWriterUnitTests.java new file mode 100644 index 0000000000..3e85ed2880 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/SpringJsonWriterUnitTests.java @@ -0,0 +1,159 @@ +/* + * 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.json; + +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; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public 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}]"); + } + +} From 40f21833e368b056f9e0f3e17d92b66aef39357d Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 13 Jan 2025 15:36:58 +0100 Subject: [PATCH 05/13] just some stuff to see if it works the way we expect it to do --- .../mongodb/aot/generated/MongoBlocks.java | 169 ++++++++++++++++++ .../generated/MongoRepositoryContributor.java | 63 ++----- .../test/java/example/aot/UserRepository.java | 13 ++ 3 files changed, 195 insertions(+), 50 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java new file mode 100644 index 0000000000..c34ba4c04e --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java @@ -0,0 +1,169 @@ +/* + * 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.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.data.mongodb.BindableMongoExpression; +import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.mongodb.repository.Hint; +import org.springframework.data.mongodb.repository.ReadPreference; +import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder.MethodGenerationMetadata; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.CodeBlock.Builder; +import org.springframework.javapoet.TypeName; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * @author Christoph Strobl + */ +public class MongoBlocks { + + public static QueryBlockBuilder queryBlockBuilder(RepositoryInformation repositoryInformation, + MethodGenerationMetadata metadata) { + return new QueryBlockBuilder(repositoryInformation, metadata); + } + + public static QueryExecutionBlockBuilder queryExecutionBlockBuilder(RepositoryInformation repositoryInformation, + MethodGenerationMetadata metadata) { + return new QueryExecutionBlockBuilder(repositoryInformation, metadata); + } + + static class QueryExecutionBlockBuilder { + + RepositoryInformation repositoryInformation; + MethodGenerationMetadata metadata; + + public QueryExecutionBlockBuilder(RepositoryInformation repositoryInformation, MethodGenerationMetadata metadata) { + this.repositoryInformation = repositoryInformation; + this.metadata = metadata; + } + + CodeBlock build(String queryVariableName) { + + String mongoOpsRef = metadata.fieldNameOf(MongoOperations.class); + + Builder builder = CodeBlock.builder(); + + boolean isProjecting = metadata.getActualReturnType() != null && !ObjectUtils + .nullSafeEquals(TypeName.get(repositoryInformation.getDomainType()), metadata.getActualReturnType()); + Object actualReturnType = isProjecting ? metadata.getActualReturnType() : repositoryInformation.getDomainType(); + + if (isProjecting) { + builder.addStatement("$T<$T> finder = $L.query($T.class).as($T.class).matching($L)", TerminatingFind.class, + actualReturnType, mongoOpsRef, repositoryInformation.getDomainType(), actualReturnType, queryVariableName); + } else { + builder.addStatement("$T<$T> finder = $L.query($T.class).matching($L)", TerminatingFind.class, actualReturnType, + mongoOpsRef, repositoryInformation.getDomainType(), queryVariableName); + } + + String terminatingMethod = "all()"; + if (metadata.returnsSingleValue()) { + if (metadata.returnsOptionalValue()) { + terminatingMethod = "one()"; + } else { + terminatingMethod = "oneValue()"; + } + } + + if (!metadata.returnsPage()) { + builder.addStatement("return finder.$L", terminatingMethod); + } else { + builder.addStatement("return $T.getPage(finder.$L, $L, () -> finder.count())", PageableExecutionUtils.class, terminatingMethod, + metadata.getPageableParameterName()); + } + + return builder.build(); + } + + } + + static class QueryBlockBuilder { + + MethodGenerationMetadata metadata; + String queryString; + List arguments; + // MongoParameters argumentSource; + + public QueryBlockBuilder(RepositoryInformation repositoryInformation, MethodGenerationMetadata metadata) { + this.metadata = metadata; + this.arguments = Arrays.stream(metadata.getRepositoryMethod().getParameters()).map(Parameter::getName) + .collect(Collectors.toList()); + + // ParametersSource parametersSource = ParametersSource.of(repositoryInformation, metadata.getRepositoryMethod()); + // this.argumentSource = new MongoParameters(parametersSource, false); + + } + + public QueryBlockBuilder filter(String filter) { + this.queryString = filter; + return this; + } + + CodeBlock build(String queryVariableName) { + + String mongoOpsRef = metadata.fieldNameOf(MongoOperations.class); + + CodeBlock.Builder builder = CodeBlock.builder(); + + builder.addStatement("$T filter = new $T($S, $L.getConverter(), new $T[]{ $L })", BindableMongoExpression.class, + BindableMongoExpression.class, queryString, mongoOpsRef, Object.class, + StringUtils.collectionToCommaDelimitedString(arguments)); + builder.addStatement("$T $L = new $T(filter.toDocument())", + org.springframework.data.mongodb.core.query.Query.class, queryVariableName, BasicQuery.class); + + String sortParameter = metadata.getSortParameterName(); + if (StringUtils.hasText(sortParameter)) { + builder.addStatement("$L.with($L)", queryVariableName, sortParameter); + } + + String limitParameter = metadata.getLimitParameterName(); + if (StringUtils.hasText(limitParameter)) { + builder.addStatement("$L.limit($L)", queryVariableName, limitParameter); + } + + String pageableParameter = metadata.getPageableParameterName(); + if (StringUtils.hasText(pageableParameter)) { + builder.addStatement("$L.with($L)", queryVariableName, pageableParameter); + } + + String hint = metadata.annotationValue(Hint.class, "value"); + + if (StringUtils.hasText(hint)) { + builder.addStatement("$L.withHint($S)", queryVariableName, hint); + } + + String readPreference = metadata.annotationValue(ReadPreference.class, "value"); + if (StringUtils.hasText(readPreference)) { + builder.addStatement("$L.withReadPreference($T.valueOf($S))", queryVariableName, + com.mongodb.ReadPreference.class, readPreference); + } + + // TODO: all the meta stuff + + return builder.build(); + } + + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java index ea17ffb774..2d209b73d1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java @@ -15,13 +15,10 @@ */ package org.springframework.data.mongodb.aot.generated; -import java.lang.reflect.Parameter; import java.util.Arrays; import java.util.Iterator; -import java.util.stream.Collectors; import java.util.stream.IntStream; -import org.apache.commons.logging.Log; import org.bson.conversions.Bson; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.domain.Pageable; @@ -30,13 +27,11 @@ import org.springframework.data.domain.Sort; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; -import org.springframework.data.mongodb.BindableMongoExpression; import org.springframework.data.mongodb.core.MongoOperations; 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.BasicQuery; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder; import org.springframework.data.mongodb.core.query.TextCriteria; @@ -49,6 +44,8 @@ import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder; import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder; import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder.MethodGenerationMetadata; +import org.springframework.data.repository.aot.generate.CodeBlocks; +import org.springframework.data.repository.aot.generate.Contribution; import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.core.RepositoryInformation; @@ -57,8 +54,6 @@ import org.springframework.javapoet.MethodSpec.Builder; import org.springframework.javapoet.TypeName; import org.springframework.lang.Nullable; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; import com.mongodb.DBRef; @@ -78,16 +73,14 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB } @Override - protected void customizeDerivedMethod(AotRepositoryMethodBuilder methodBuilder) { + protected Contribution customizeDerivedMethod(AotRepositoryMethodBuilder methodBuilder) { methodBuilder.customize((repositoryInformation, metadata, body) -> { Query query = AnnotatedElementUtils.findMergedAnnotation(metadata.getRepositoryMethod(), Query.class); if (query != null) { - userAnnotatedQuery(repositoryInformation, metadata, body, query); + userAnnotatedQuery(repositoryInformation, metadata, methodBuilder.codeBlocks(), body, query); } else { - ; - MongoMappingContext mongoMappingContext = new MongoMappingContext(); mongoMappingContext.setSimpleTypeHolder( MongoCustomConversions.create((cfg) -> cfg.useNativeDriverJavaTimeCodecs()).getSimpleTypeHolder()); @@ -195,52 +188,22 @@ public Iterator iterator() { org.springframework.data.mongodb.core.query.Query partTreeQuery = queryCreator.createQuery(); StringBuffer buffer = new StringBuffer(); BsonUtils.writeJson(partTreeQuery.getQueryObject()).to(buffer); - writeStringQuery(repositoryInformation, metadata, body, buffer.toString()); + writeStringQuery(repositoryInformation, metadata, methodBuilder.codeBlocks(), body, buffer.toString()); } }); + return Contribution.CODE; } private static void writeStringQuery(RepositoryInformation repositoryInformation, MethodGenerationMetadata metadata, - Builder body, String query) { - - String mongoOpsRef = metadata.fieldNameOf(MongoOperations.class); - String arguments = StringUtils.collectionToCommaDelimitedString(Arrays - .stream(metadata.getRepositoryMethod().getParameters()).map(Parameter::getName).collect(Collectors.toList())); - - body.beginControlFlow("if($L.isDebugEnabled())", metadata.fieldNameOf(Log.class)); - body.addStatement("$L.debug(\"invoking generated [$L] method\")", metadata.fieldNameOf(Log.class), - metadata.getRepositoryMethod().getName()); - body.endControlFlow(); - - body.addStatement("$T filter = new $T($S, $L.getConverter(), new $T[]{ $L })", BindableMongoExpression.class, - BindableMongoExpression.class, query, mongoOpsRef, Object.class, arguments); - body.addStatement("$T query = new $T(filter.toDocument())", org.springframework.data.mongodb.core.query.Query.class, - BasicQuery.class); - - boolean isCollectionType = TypeInformation.fromReturnTypeOf(metadata.getRepositoryMethod()).isCollectionLike(); - String terminatingMethod = isCollectionType ? "all()" : "oneValue()"; - - if (metadata.getActualReturnType() != null && ObjectUtils - .nullSafeEquals(TypeName.get(repositoryInformation.getDomainType()), metadata.getActualReturnType())) { - body.addStatement(""" - return $L.query($T.class) - .matching(query) - .$L""", mongoOpsRef, repositoryInformation.getDomainType(), terminatingMethod); - } else { - - body.addStatement(""" - return $L.query($T.class) - .as($T.class) - .matching(query) - .$L""", mongoOpsRef, repositoryInformation.getDomainType(), - metadata.getActualReturnType() != null ? metadata.getActualReturnType() - : repositoryInformation.getDomainType(), - terminatingMethod); - } + CodeBlocks codeBlocks, Builder body, String query) { + + body.addCode(codeBlocks.logDebug("invoking [%s]".formatted(metadata.getRepositoryMethod().getName()))); + body.addCode(MongoBlocks.queryBlockBuilder(repositoryInformation, metadata).filter(query).build("query")); + body.addCode(MongoBlocks.queryExecutionBlockBuilder(repositoryInformation, metadata).build("query")); } private static void userAnnotatedQuery(RepositoryInformation repositoryInformation, MethodGenerationMetadata metadata, - Builder body, Query query) { - writeStringQuery(repositoryInformation, metadata, body, query.value()); + CodeBlocks codeBlocks, Builder body, Query query) { + writeStringQuery(repositoryInformation, metadata, codeBlocks, body, query.value()); } } diff --git a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java index 441cd3da83..7d13e66370 100644 --- a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java +++ b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java @@ -17,7 +17,12 @@ import java.util.List; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.repository.Query; +import org.springframework.data.mongodb.repository.ReadPreference; import org.springframework.data.repository.CrudRepository; /** @@ -30,7 +35,15 @@ public interface UserRepository extends CrudRepository { @Query("{ 'username' : '?0' }") List findAllByAnnotatedQueryWithParameter(String username); + @ReadPreference("secondary") User findByUsername(String username); List findUserByLastnameLike(String lastname); + + List findUserByLastnameStartingWith(String lastname, Pageable page); + List findUserByLastnameStartingWith(String lastname, Sort sort); + List findUserByLastnameStartingWith(String lastname, Limit limit); + List findUserByLastnameStartingWith(String lastname, Sort sort, Limit limit); + + Page findUserByFirstnameStartingWith(String lastname, Pageable page); } From 15e207e9c489ed2d0f77b4eebff3eec8a42a71dd Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 14 Jan 2025 09:50:06 +0100 Subject: [PATCH 06/13] switch to query execution maybe? --- .../mongodb/aot/generated/MongoBlocks.java | 36 +++++++++++++------ .../repository/query/MongoQueryExecution.java | 22 ++++++------ .../test/java/example/aot/UserRepository.java | 4 ++- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java index c34ba4c04e..f1e2d38fa1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java @@ -21,14 +21,15 @@ import java.util.stream.Collectors; import org.springframework.data.mongodb.BindableMongoExpression; -import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind; +import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.repository.Hint; import org.springframework.data.mongodb.repository.ReadPreference; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagedExecution; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SlicedExecution; import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder.MethodGenerationMetadata; import org.springframework.data.repository.core.RepositoryInformation; -import org.springframework.data.support.PageableExecutionUtils; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; import org.springframework.javapoet.TypeName; @@ -71,11 +72,16 @@ CodeBlock build(String queryVariableName) { Object actualReturnType = isProjecting ? metadata.getActualReturnType() : repositoryInformation.getDomainType(); if (isProjecting) { - builder.addStatement("$T<$T> finder = $L.query($T.class).as($T.class).matching($L)", TerminatingFind.class, - actualReturnType, mongoOpsRef, repositoryInformation.getDomainType(), actualReturnType, queryVariableName); + // builder.addStatement("$T<$T> finder = $L.query($T.class).as($T.class).matching($L)", TerminatingFind.class, + // actualReturnType, mongoOpsRef, repositoryInformation.getDomainType(), actualReturnType, queryVariableName); + builder.addStatement("$T<$T> finder = $L.query($T.class).as($T.class)", FindWithQuery.class, actualReturnType, + mongoOpsRef, repositoryInformation.getDomainType(), actualReturnType); } else { - builder.addStatement("$T<$T> finder = $L.query($T.class).matching($L)", TerminatingFind.class, actualReturnType, - mongoOpsRef, repositoryInformation.getDomainType(), queryVariableName); + // builder.addStatement("$T<$T> finder = $L.query($T.class).matching($L)", TerminatingFind.class, + // actualReturnType, + // mongoOpsRef, repositoryInformation.getDomainType(), queryVariableName); + builder.addStatement("$T<$T> finder = $L.query($T.class)", FindWithQuery.class, actualReturnType, mongoOpsRef, + repositoryInformation.getDomainType()); } String terminatingMethod = "all()"; @@ -87,14 +93,24 @@ CodeBlock build(String queryVariableName) { } } - if (!metadata.returnsPage()) { - builder.addStatement("return finder.$L", terminatingMethod); + if (metadata.returnsPage()) { + // builder.addStatement("return finder.$L", terminatingMethod); + builder.addStatement("return new $T(finder, $L).execute($L)", PagedExecution.class, + metadata.getPageableParameterName(), queryVariableName); + } else if (metadata.returnsSlice()) { + builder.addStatement("return new $T(finder, $L).execute($L)", SlicedExecution.class, + metadata.getPageableParameterName(), queryVariableName); } else { - builder.addStatement("return $T.getPage(finder.$L, $L, () -> finder.count())", PageableExecutionUtils.class, terminatingMethod, - metadata.getPageableParameterName()); + builder.addStatement("return finder.matching($L).$L", queryVariableName, terminatingMethod); + // builder.addStatement("return $T.getPage(finder.$L, $L, () -> finder.count())", PageableExecutionUtils.class, + // terminatingMethod, + // metadata.getPageableParameterName()); } + // new MongoQueryExecution.PagedExecution(finder, page).execute(query); + return builder.build(); + } } 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 cebdf4e408..f1cbff5889 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 @@ -54,7 +54,7 @@ * @author Christoph Strobl */ @FunctionalInterface -interface MongoQueryExecution { +public interface MongoQueryExecution { @Nullable Object execute(Query query); @@ -66,12 +66,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"); @@ -82,7 +82,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(); @@ -92,7 +92,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); } } @@ -103,12 +103,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"); @@ -118,11 +118,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); diff --git a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java index 7d13e66370..d1dc7298dd 100644 --- a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java +++ b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Limit; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.repository.Query; import org.springframework.data.mongodb.repository.ReadPreference; @@ -45,5 +46,6 @@ public interface UserRepository extends CrudRepository { List findUserByLastnameStartingWith(String lastname, Limit limit); List findUserByLastnameStartingWith(String lastname, Sort sort, Limit limit); - Page findUserByFirstnameStartingWith(String lastname, Pageable page); + Page findUserByFirstnameStartingWith(String firstname, Pageable page); + Slice findUserByFirstnameLike(String firstname, Pageable page); } From 7e78dac23bf8578d15f449de682c7243ca3544a7 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 14 Jan 2025 12:28:14 +0100 Subject: [PATCH 07/13] Update code generation to latest changes in commons. --- .../mongodb/aot/generated/MongoBlocks.java | 67 +++++++++---------- .../generated/MongoRepositoryContributor.java | 39 +++++------ 2 files changed, 49 insertions(+), 57 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java index f1e2d38fa1..fadae45f0b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java @@ -28,8 +28,7 @@ import org.springframework.data.mongodb.repository.ReadPreference; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagedExecution; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SlicedExecution; -import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder.MethodGenerationMetadata; -import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; import org.springframework.javapoet.TypeName; @@ -41,65 +40,63 @@ */ public class MongoBlocks { - public static QueryBlockBuilder queryBlockBuilder(RepositoryInformation repositoryInformation, - MethodGenerationMetadata metadata) { - return new QueryBlockBuilder(repositoryInformation, metadata); + public static QueryBlockBuilder queryBlockBuilder(AotRepositoryMethodGenerationContext context) { + return new QueryBlockBuilder(context); } - public static QueryExecutionBlockBuilder queryExecutionBlockBuilder(RepositoryInformation repositoryInformation, - MethodGenerationMetadata metadata) { - return new QueryExecutionBlockBuilder(repositoryInformation, metadata); + public static QueryExecutionBlockBuilder queryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { + return new QueryExecutionBlockBuilder(context); } static class QueryExecutionBlockBuilder { - RepositoryInformation repositoryInformation; - MethodGenerationMetadata metadata; + AotRepositoryMethodGenerationContext context; - public QueryExecutionBlockBuilder(RepositoryInformation repositoryInformation, MethodGenerationMetadata metadata) { - this.repositoryInformation = repositoryInformation; - this.metadata = metadata; + public QueryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { + this.context = context; } CodeBlock build(String queryVariableName) { - String mongoOpsRef = metadata.fieldNameOf(MongoOperations.class); + String mongoOpsRef = context.fieldNameOf(MongoOperations.class); Builder builder = CodeBlock.builder(); - boolean isProjecting = metadata.getActualReturnType() != null && !ObjectUtils - .nullSafeEquals(TypeName.get(repositoryInformation.getDomainType()), metadata.getActualReturnType()); - Object actualReturnType = isProjecting ? metadata.getActualReturnType() : repositoryInformation.getDomainType(); + boolean isProjecting = context.getActualReturnType() != null + && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), + context.getActualReturnType()); + Object actualReturnType = isProjecting ? context.getActualReturnType() + : context.getRepositoryInformation().getDomainType(); if (isProjecting) { // builder.addStatement("$T<$T> finder = $L.query($T.class).as($T.class).matching($L)", TerminatingFind.class, // actualReturnType, mongoOpsRef, repositoryInformation.getDomainType(), actualReturnType, queryVariableName); builder.addStatement("$T<$T> finder = $L.query($T.class).as($T.class)", FindWithQuery.class, actualReturnType, - mongoOpsRef, repositoryInformation.getDomainType(), actualReturnType); + mongoOpsRef, context.getRepositoryInformation().getDomainType(), actualReturnType); } else { // builder.addStatement("$T<$T> finder = $L.query($T.class).matching($L)", TerminatingFind.class, // actualReturnType, // mongoOpsRef, repositoryInformation.getDomainType(), queryVariableName); builder.addStatement("$T<$T> finder = $L.query($T.class)", FindWithQuery.class, actualReturnType, mongoOpsRef, - repositoryInformation.getDomainType()); + context.getRepositoryInformation().getDomainType()); } String terminatingMethod = "all()"; - if (metadata.returnsSingleValue()) { - if (metadata.returnsOptionalValue()) { + if (context.returnsSingleValue()) { + if (context.returnsOptionalValue()) { terminatingMethod = "one()"; } else { terminatingMethod = "oneValue()"; } } - if (metadata.returnsPage()) { + if (context.returnsPage()) { // builder.addStatement("return finder.$L", terminatingMethod); builder.addStatement("return new $T(finder, $L).execute($L)", PagedExecution.class, - metadata.getPageableParameterName(), queryVariableName); - } else if (metadata.returnsSlice()) { + context.getPageableParameterName(), queryVariableName); + } else if (context.returnsSlice()) { builder.addStatement("return new $T(finder, $L).execute($L)", SlicedExecution.class, - metadata.getPageableParameterName(), queryVariableName); + context.getPageableParameterName(), queryVariableName); } else { builder.addStatement("return finder.matching($L).$L", queryVariableName, terminatingMethod); // builder.addStatement("return $T.getPage(finder.$L, $L, () -> finder.count())", PageableExecutionUtils.class, @@ -117,14 +114,14 @@ CodeBlock build(String queryVariableName) { static class QueryBlockBuilder { - MethodGenerationMetadata metadata; + AotRepositoryMethodGenerationContext context; String queryString; List arguments; // MongoParameters argumentSource; - public QueryBlockBuilder(RepositoryInformation repositoryInformation, MethodGenerationMetadata metadata) { - this.metadata = metadata; - this.arguments = Arrays.stream(metadata.getRepositoryMethod().getParameters()).map(Parameter::getName) + public QueryBlockBuilder(AotRepositoryMethodGenerationContext context) { + this.context = context; + this.arguments = Arrays.stream(context.getMethod().getParameters()).map(Parameter::getName) .collect(Collectors.toList()); // ParametersSource parametersSource = ParametersSource.of(repositoryInformation, metadata.getRepositoryMethod()); @@ -139,7 +136,7 @@ public QueryBlockBuilder filter(String filter) { CodeBlock build(String queryVariableName) { - String mongoOpsRef = metadata.fieldNameOf(MongoOperations.class); + String mongoOpsRef = context.fieldNameOf(MongoOperations.class); CodeBlock.Builder builder = CodeBlock.builder(); @@ -149,28 +146,28 @@ CodeBlock build(String queryVariableName) { builder.addStatement("$T $L = new $T(filter.toDocument())", org.springframework.data.mongodb.core.query.Query.class, queryVariableName, BasicQuery.class); - String sortParameter = metadata.getSortParameterName(); + String sortParameter = context.getSortParameterName(); if (StringUtils.hasText(sortParameter)) { builder.addStatement("$L.with($L)", queryVariableName, sortParameter); } - String limitParameter = metadata.getLimitParameterName(); + String limitParameter = context.getLimitParameterName(); if (StringUtils.hasText(limitParameter)) { builder.addStatement("$L.limit($L)", queryVariableName, limitParameter); } - String pageableParameter = metadata.getPageableParameterName(); + String pageableParameter = context.getPageableParameterName(); if (StringUtils.hasText(pageableParameter)) { builder.addStatement("$L.with($L)", queryVariableName, pageableParameter); } - String hint = metadata.annotationValue(Hint.class, "value"); + String hint = context.annotationValue(Hint.class, "value"); if (StringUtils.hasText(hint)) { builder.addStatement("$L.withHint($S)", queryVariableName, hint); } - String readPreference = metadata.annotationValue(ReadPreference.class, "value"); + String readPreference = context.annotationValue(ReadPreference.class, "value"); if (StringUtils.hasText(readPreference)) { builder.addStatement("$L.withReadPreference($T.valueOf($S))", queryVariableName, com.mongodb.ReadPreference.class, readPreference); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java index 2d209b73d1..a2dd69f99b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java @@ -43,12 +43,9 @@ import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder; import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder; -import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder.MethodGenerationMetadata; -import org.springframework.data.repository.aot.generate.CodeBlocks; -import org.springframework.data.repository.aot.generate.Contribution; +import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext; 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.query.parser.PartTree; import org.springframework.data.util.TypeInformation; import org.springframework.javapoet.MethodSpec.Builder; @@ -73,12 +70,13 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB } @Override - protected Contribution customizeDerivedMethod(AotRepositoryMethodBuilder methodBuilder) { + protected AotRepositoryMethodBuilder contributeRepositoryMethod( + AotRepositoryMethodGenerationContext generationContext) { - methodBuilder.customize((repositoryInformation, metadata, body) -> { - Query query = AnnotatedElementUtils.findMergedAnnotation(metadata.getRepositoryMethod(), Query.class); + return new AotRepositoryMethodBuilder(generationContext).customize((context, body) -> { + Query query = AnnotatedElementUtils.findMergedAnnotation(context.getMethod(), Query.class); if (query != null) { - userAnnotatedQuery(repositoryInformation, metadata, methodBuilder.codeBlocks(), body, query); + userAnnotatedQuery(context, body, query); } else { MongoMappingContext mongoMappingContext = new MongoMappingContext(); @@ -87,8 +85,8 @@ protected Contribution customizeDerivedMethod(AotRepositoryMethodBuilder methodB mongoMappingContext.setAutoIndexCreation(false); mongoMappingContext.afterPropertiesSet(); - PartTree partTree = new PartTree(metadata.getRepositoryMethod().getName(), - repositoryInformation.getDomainType()); + PartTree partTree = new PartTree(context.getMethod().getName(), + context.getRepositoryInformation().getDomainType()); MongoQueryCreator queryCreator = new MongoQueryCreator(partTree, new ConvertingParameterAccessor(new MongoWriter() { @Nullable @@ -133,10 +131,10 @@ public Collation getCollation() { @Override public Object[] getValues() { - if (metadata.getRepositoryMethod().getParameterCount() == 0) { + if (context.getMethod().getParameterCount() == 0) { return new Object[] {}; } - return IntStream.range(0, metadata.getRepositoryMethod().getParameterCount()) + return IntStream.range(0, context.getMethod().getParameterCount()) .mapToObj(it -> new Placeholder("?" + it)).toArray(); } @@ -188,22 +186,19 @@ public Iterator iterator() { org.springframework.data.mongodb.core.query.Query partTreeQuery = queryCreator.createQuery(); StringBuffer buffer = new StringBuffer(); BsonUtils.writeJson(partTreeQuery.getQueryObject()).to(buffer); - writeStringQuery(repositoryInformation, metadata, methodBuilder.codeBlocks(), body, buffer.toString()); + writeStringQuery(context, body, buffer.toString()); } }); - return Contribution.CODE; } - private static void writeStringQuery(RepositoryInformation repositoryInformation, MethodGenerationMetadata metadata, - CodeBlocks codeBlocks, Builder body, String query) { + private static void writeStringQuery(AotRepositoryMethodGenerationContext context, Builder body, String query) { - body.addCode(codeBlocks.logDebug("invoking [%s]".formatted(metadata.getRepositoryMethod().getName()))); - body.addCode(MongoBlocks.queryBlockBuilder(repositoryInformation, metadata).filter(query).build("query")); - body.addCode(MongoBlocks.queryExecutionBlockBuilder(repositoryInformation, metadata).build("query")); + body.addCode(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); + body.addCode(MongoBlocks.queryBlockBuilder(context).filter(query).build("query")); + body.addCode(MongoBlocks.queryExecutionBlockBuilder(context).build("query")); } - private static void userAnnotatedQuery(RepositoryInformation repositoryInformation, MethodGenerationMetadata metadata, - CodeBlocks codeBlocks, Builder body, Query query) { - writeStringQuery(repositoryInformation, metadata, codeBlocks, body, query.value()); + private static void userAnnotatedQuery(AotRepositoryMethodGenerationContext context, Builder body, Query query) { + writeStringQuery(context, body, query.value()); } } From 2770a67bac98b24c6376ef4b0d59d3b74a9b8270 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 15 Jan 2025 13:56:29 +0100 Subject: [PATCH 08/13] Support count and exists queries as well as field projections and topN limit --- .../aot/generated/AotQueryCreator.java | 199 +++++++++++++++ .../mongodb/aot/generated/MongoBlocks.java | 74 +++++- .../generated/MongoRepositoryContributor.java | 180 ++++---------- .../mongodb/aot/generated/StringQuery.java | 227 ++++++++++++++++++ .../aot/AotMongoRepositoryPostProcessor.java | 1 + .../mongodb/util/json/SpringJsonWriter.java | 8 +- .../test/java/example/aot/UserProjection.java | 29 +++ .../test/java/example/aot/UserRepository.java | 22 ++ .../util/json/SpringJsonWriterUnitTests.java | 12 +- 9 files changed, 594 insertions(+), 158 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/AotQueryCreator.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/StringQuery.java create mode 100644 spring-data-mongodb/src/test/java/example/aot/UserProjection.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/AotQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/AotQueryCreator.java new file mode 100644 index 0000000000..c0fbfc4ee9 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/AotQueryCreator.java @@ -0,0 +1,199 @@ +/* + * 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.Iterator; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.bson.conversions.Bson; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Sort; +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 org.springframework.lang.Nullable; + +import com.mongodb.DBRef; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public 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; + } + + 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); + } + } + + enum PlaceholderWriter implements MongoWriter { + + INSTANCE; + + @Nullable + @Override + public 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) { + + } + } + + 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(it -> new Placeholder("?" + it)) + .collect(Collectors.toList()); + } + } + + @Override + public Range getDistanceRange() { + return null; + } + + @Nullable + @Override + public Point getGeoNearLocation() { + return null; + } + + @Nullable + @Override + public TextCriteria getFullText() { + return null; + } + + @Nullable + @Override + public Collation getCollation() { + return null; + } + + @Override + public Object[] getValues() { + return placeholders.toArray(); + } + + @Nullable + @Override + public UpdateDefinition getUpdate() { + return null; + } + + @Nullable + @Override + public ScrollPosition getScrollPosition() { + return null; + } + + @Override + public Pageable getPageable() { + return null; + } + + @Override + public Sort getSort() { + return null; + } + + @Nullable + @Override + public Class findDynamicProjection() { + return null; + } + + @Nullable + @Override + public 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/aot/generated/MongoBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java index fadae45f0b..c4c4050802 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java @@ -18,8 +18,10 @@ import java.lang.reflect.Parameter; import java.util.Arrays; import java.util.List; +import java.util.regex.Pattern; import java.util.stream.Collectors; +import org.bson.Document; import org.springframework.data.mongodb.BindableMongoExpression; import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; import org.springframework.data.mongodb.core.MongoOperations; @@ -32,6 +34,7 @@ import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; import org.springframework.javapoet.TypeName; +import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -40,6 +43,8 @@ */ public class MongoBlocks { + private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)"); + public static QueryBlockBuilder queryBlockBuilder(AotRepositoryMethodGenerationContext context) { return new QueryBlockBuilder(context); } @@ -68,6 +73,7 @@ CodeBlock build(String queryVariableName) { Object actualReturnType = isProjecting ? context.getActualReturnType() : context.getRepositoryInformation().getDomainType(); + builder.add("\n"); if (isProjecting) { // builder.addStatement("$T<$T> finder = $L.query($T.class).as($T.class).matching($L)", TerminatingFind.class, // actualReturnType, mongoOpsRef, repositoryInformation.getDomainType(), actualReturnType, queryVariableName); @@ -83,8 +89,13 @@ CodeBlock build(String queryVariableName) { String terminatingMethod = "all()"; if (context.returnsSingleValue()) { + if (context.returnsOptionalValue()) { terminatingMethod = "one()"; + } else if(context.isCountMethod()){ + terminatingMethod = "count()"; + } else if(context.isExistsMethod()){ + terminatingMethod = "exists()"; } else { terminatingMethod = "oneValue()"; } @@ -115,8 +126,9 @@ CodeBlock build(String queryVariableName) { static class QueryBlockBuilder { AotRepositoryMethodGenerationContext context; - String queryString; + StringQuery source; List arguments; + // MongoParameters argumentSource; public QueryBlockBuilder(AotRepositoryMethodGenerationContext context) { @@ -129,35 +141,47 @@ public QueryBlockBuilder(AotRepositoryMethodGenerationContext context) { } - public QueryBlockBuilder filter(String filter) { - this.queryString = filter; + public QueryBlockBuilder filter(StringQuery query) { + this.source = query; return this; } CodeBlock build(String queryVariableName) { - String mongoOpsRef = context.fieldNameOf(MongoOperations.class); - CodeBlock.Builder builder = CodeBlock.builder(); - builder.addStatement("$T filter = new $T($S, $L.getConverter(), new $T[]{ $L })", BindableMongoExpression.class, - BindableMongoExpression.class, queryString, mongoOpsRef, Object.class, - StringUtils.collectionToCommaDelimitedString(arguments)); - builder.addStatement("$T $L = new $T(filter.toDocument())", - org.springframework.data.mongodb.core.query.Query.class, queryVariableName, BasicQuery.class); + builder.add("\n"); + // String queryStringName = "%sString".formatted(queryVariableName); + // builder.addStatement("String $L = $S", queryStringName, queryString); + String queryDocumentVariableName = "%sDocument".formatted(queryVariableName); + builder.add(renderExpressionToDocument(source.getQueryString(), queryVariableName)); + builder.addStatement("$T $L = new $T($L)", BasicQuery.class, queryVariableName, BasicQuery.class, + queryDocumentVariableName); + + if (StringUtils.hasText(source.getFieldsString())) { + builder.add(renderExpressionToDocument(source.getFieldsString(), "fields")); + builder.addStatement("$L.setFieldsObject(fieldsDocument)", queryVariableName); + } String sortParameter = context.getSortParameterName(); if (StringUtils.hasText(sortParameter)) { + builder.addStatement("$L.with($L)", queryVariableName, sortParameter); + } else if (StringUtils.hasText(source.getSortString())) { + + builder.add(renderExpressionToDocument(source.getSortString(), "sort")); + builder.addStatement("$L.setSortObject(sortDocument)", queryVariableName); } String limitParameter = context.getLimitParameterName(); if (StringUtils.hasText(limitParameter)) { builder.addStatement("$L.limit($L)", queryVariableName, limitParameter); + } else if (context.getPageableParameterName() == null && source.isLimited()) { + builder.addStatement("$L.limit($L)", queryVariableName, source.getLimit()); } String pageableParameter = context.getPageableParameterName(); - if (StringUtils.hasText(pageableParameter)) { + if (StringUtils.hasText(pageableParameter) && (!context.returnsPage() || !context.returnsSlice())) { builder.addStatement("$L.with($L)", queryVariableName, pageableParameter); } @@ -178,5 +202,33 @@ CodeBlock build(String queryVariableName) { return builder.build(); } + private CodeBlock renderExpressionToDocument(@Nullable String source, String variableName) { + + Builder builder = CodeBlock.builder(); + if(!StringUtils.hasText(source)) { + builder.addStatement("$T $L = new $T()", Document.class, "%sDocument".formatted(variableName), + Document.class); + } + else if (!containsPlaceholder(source)) { + builder.addStatement("$T $L = $T.parse($S)", Document.class, "%sDocument".formatted(variableName), + Document.class, source); + } else { + + String mongoOpsRef = context.fieldNameOf(MongoOperations.class); + String tmpVarName = "%sString".formatted(variableName); + + builder.addStatement("String $L = $S", tmpVarName, source); + builder.addStatement("$T $L = new $T($L, $L.getConverter(), new $T[]{ $L }).toDocument()", Document.class, + "%sDocument".formatted(variableName), BindableMongoExpression.class, tmpVarName, mongoOpsRef, Object.class, + StringUtils.collectionToDelimitedString(arguments, ", ")); + } + + return builder.build(); + } + + private boolean containsPlaceholder(String source) { + return PARAMETER_BINDING_PATTERN.matcher(source).find(); + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java index a2dd69f99b..85d3bfa015 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java @@ -15,44 +15,21 @@ */ package org.springframework.data.mongodb.aot.generated; -import java.util.Arrays; -import java.util.Iterator; -import java.util.stream.IntStream; +import java.util.regex.Pattern; -import org.bson.conversions.Bson; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Range; -import org.springframework.data.domain.ScrollPosition; -import org.springframework.data.domain.Sort; -import org.springframework.data.geo.Distance; -import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.MongoOperations; -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.TextCriteria; -import org.springframework.data.mongodb.core.query.UpdateDefinition; +import org.springframework.data.mongodb.repository.Aggregation; import org.springframework.data.mongodb.repository.Query; -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.mongodb.util.BsonUtils; import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder; import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder; import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext; import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.query.parser.PartTree; -import org.springframework.data.util.TypeInformation; import org.springframework.javapoet.MethodSpec.Builder; import org.springframework.javapoet.TypeName; -import org.springframework.lang.Nullable; - -import com.mongodb.DBRef; +import org.springframework.util.StringUtils; /** * @author Christoph Strobl @@ -60,8 +37,11 @@ */ public class MongoRepositoryContributor extends RepositoryContributor { + private AotQueryCreator queryCreator; + public MongoRepositoryContributor(AotRepositoryContext repositoryContext) { super(repositoryContext); + this.queryCreator = new AotQueryCreator(); } @Override @@ -73,125 +53,51 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB protected AotRepositoryMethodBuilder contributeRepositoryMethod( AotRepositoryMethodGenerationContext generationContext) { + // TODO: do not generate stuff for spel expressions + + // skip currently unsupported Stuff. + if (generationContext.isDeleteMethod()) { + return null; + } + if (AnnotatedElementUtils.hasAnnotation(generationContext.getMethod(), Aggregation.class)) { + return null; + } + { + Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(generationContext.getMethod(), Query.class); + if (queryAnnotation != null) { + if (StringUtils.hasText(queryAnnotation.value()) + && Pattern.compile("[\\?:][#$]\\{.*\\}").matcher(queryAnnotation.value()).find()) { + return null; + } + } + } + + // so the rest should work return new AotRepositoryMethodBuilder(generationContext).customize((context, body) -> { - Query query = AnnotatedElementUtils.findMergedAnnotation(context.getMethod(), Query.class); - if (query != null) { - userAnnotatedQuery(context, body, query); - } else { - MongoMappingContext mongoMappingContext = new MongoMappingContext(); - mongoMappingContext.setSimpleTypeHolder( - MongoCustomConversions.create((cfg) -> cfg.useNativeDriverJavaTimeCodecs()).getSimpleTypeHolder()); - mongoMappingContext.setAutoIndexCreation(false); - mongoMappingContext.afterPropertiesSet(); + Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(context.getMethod(), Query.class); + StringQuery query; + if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.value())) { + query = new StringQuery(queryAnnotation.value()); + } else { PartTree partTree = new PartTree(context.getMethod().getName(), context.getRepositoryInformation().getDomainType()); - MongoQueryCreator queryCreator = new MongoQueryCreator(partTree, - new ConvertingParameterAccessor(new MongoWriter() { - @Nullable - @Override - public Object convertToMongoType(@Nullable Object obj, @Nullable TypeInformation typeInformation) { - return "?0"; - } - - @Override - public DBRef toDBRef(Object object, @Nullable MongoPersistentProperty referringProperty) { - return null; - } - - @Override - public void write(Object source, Bson sink) { - - } - }, new MongoParameterAccessor() { - @Override - public Range getDistanceRange() { - return null; - } - - @Nullable - @Override - public Point getGeoNearLocation() { - return null; - } - - @Nullable - @Override - public TextCriteria getFullText() { - return null; - } - - @Nullable - @Override - public Collation getCollation() { - return null; - } - - @Override - public Object[] getValues() { - - if (context.getMethod().getParameterCount() == 0) { - return new Object[] {}; - } - return IntStream.range(0, context.getMethod().getParameterCount()) - .mapToObj(it -> new Placeholder("?" + it)).toArray(); - } - - @Nullable - @Override - public UpdateDefinition getUpdate() { - return null; - } - - @Nullable - @Override - public ScrollPosition getScrollPosition() { - return null; - } - - @Override - public Pageable getPageable() { - return null; - } - - @Override - public Sort getSort() { - return null; - } - - @Nullable - @Override - public Class findDynamicProjection() { - return null; - } - - @Nullable - @Override - public Object getBindableValue(int index) { - return "?" + index; - } - - @Override - public boolean hasBindableNullValue() { - return false; - } - - @Override - public Iterator iterator() { - return Arrays.stream(getValues()).iterator(); - } - }), mongoMappingContext); - - org.springframework.data.mongodb.core.query.Query partTreeQuery = queryCreator.createQuery(); - StringBuffer buffer = new StringBuffer(); - BsonUtils.writeJson(partTreeQuery.getQueryObject()).to(buffer); - writeStringQuery(context, body, buffer.toString()); + query = queryCreator.createQuery(partTree, context.getMethod().getParameterCount()); + } + + if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.sort())) { + query.sort(queryAnnotation.sort()); } + if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.fields())) { + query.fields(queryAnnotation.fields()); + } + + writeStringQuery(context, body, query); }); } - private static void writeStringQuery(AotRepositoryMethodGenerationContext context, Builder body, String query) { + private static void writeStringQuery(AotRepositoryMethodGenerationContext context, Builder body, StringQuery query) { body.addCode(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); body.addCode(MongoBlocks.queryBlockBuilder(context).filter(query).build("query")); @@ -199,6 +105,6 @@ private static void writeStringQuery(AotRepositoryMethodGenerationContext contex } private static void userAnnotatedQuery(AotRepositoryMethodGenerationContext context, Builder body, Query query) { - writeStringQuery(context, body, query.value()); + writeStringQuery(context, body, new StringQuery(query.value())); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/StringQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/StringQuery.java new file mode 100644 index 0000000000..c8d7b7ab2a --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/StringQuery.java @@ -0,0 +1,227 @@ +/* + * 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 + * + * http://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. + */ + +/* + * 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 + * + * http://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.Optional; +import java.util.Set; + +import org.bson.Document; +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.lang.Nullable; +import org.springframework.util.StringUtils; + +import com.mongodb.ReadConcern; +import com.mongodb.ReadPreference; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +class StringQuery extends Query { + + private Query delegate; + private @Nullable String raw; + private @Nullable String sort; + private @Nullable String fields; + + private ExecutionType executionType = ExecutionType.QUERY; + + public StringQuery(Query query) { + this.delegate = query; + } + + public StringQuery(String query) { + this.delegate = new Query(); + this.raw = query; + } + + public StringQuery forCount() { + this.executionType = ExecutionType.COUNT; + return this; + } + + @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 ReadConcern getReadConcern() { + return delegate.getReadConcern(); + } + + @Override + public boolean hasReadPreference() { + return delegate.hasReadPreference(); + } + + @Override + public ReadPreference getReadPreference() { + return delegate.getReadPreference(); + } + + @Override + public boolean hasKeyset() { + return delegate.hasKeyset(); + } + + @Override + @Nullable + public 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 + @Nullable + public 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) { + StringBuffer buffer = new StringBuffer(); + BsonUtils.writeJson(source).to(buffer); + return buffer.toString(); + } + + enum ExecutionType { + QUERY, COUNT, DELETE + } +} 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 a780620fc9..f529eba66a 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 @@ -43,6 +43,7 @@ protected void contribute(AotRepositoryContext repositoryContext, GenerationCont }); if (AotContext.aotGeneratedRepositoriesEnabled()) { + new MongoRepositoryContributor(repositoryContext).contribute(generationContext); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/SpringJsonWriter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/SpringJsonWriter.java index fdbf5ae4d6..370a272f53 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/SpringJsonWriter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/SpringJsonWriter.java @@ -149,7 +149,7 @@ public void writeDouble(double value) { writeStartDocument(); writeName("$numberDouble"); - buffer.append(value); + writeString(Double.valueOf(value).toString()); writeEndDocument(); } @@ -187,7 +187,7 @@ public void writeInt32(int value) { writeStartDocument(); writeName("$numberInt"); - buffer.append(value); + writeString(Integer.valueOf(value).toString()); writeEndDocument(); } @@ -203,7 +203,7 @@ public void writeInt64(long value) { writeStartDocument(); writeName("$numberLong"); - buffer.append(value); + writeString(Long.valueOf(value).toString()); writeEndDocument(); } @@ -220,7 +220,7 @@ public void writeDecimal128(Decimal128 value) { // { "$numberDecimal": "" } writeStartDocument(); writeName("$numberDecimal"); - write(value.toString()); + writeString(value.toString()); writeEndDocument(); } diff --git a/spring-data-mongodb/src/test/java/example/aot/UserProjection.java b/spring-data-mongodb/src/test/java/example/aot/UserProjection.java new file mode 100644 index 0000000000..06c70f8060 --- /dev/null +++ b/spring-data-mongodb/src/test/java/example/aot/UserProjection.java @@ -0,0 +1,29 @@ +/* + * 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; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public interface UserProjection { + + 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 index d1dc7298dd..4e89085b14 100644 --- a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java +++ b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java @@ -36,9 +36,31 @@ public interface UserRepository extends CrudRepository { @Query("{ 'username' : '?0' }") List findAllByAnnotatedQueryWithParameter(String username); + @Query(""" + { + 'username' : '?0' + }""") + List findAllByAnnotatedMultilineQueryWithParameter(String username); + @ReadPreference("secondary") User findByUsername(String username); + Page findUserProjectionBy(Pageable pageable); + + @Query(sort = "{ 'last_name' : -1}") + List findByLastnameAfter(String lastname); + + Long countUsersByLastnameLike(String lastname); + + Boolean existsUserByLastname(String lastname); + + @Query(fields = "{ '_id' : -1}") + List findByLastnameBefore(String lastname); + + List findTop5ByUsernameLike(String username); + + List findByLastnameOrderByFirstnameDesc(String lastname); + List findUserByLastnameLike(String lastname); List findUserByLastnameStartingWith(String lastname, Pageable page); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/SpringJsonWriterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/SpringJsonWriterUnitTests.java index 3e85ed2880..57b8df548d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/SpringJsonWriterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/SpringJsonWriterUnitTests.java @@ -65,7 +65,7 @@ void writeInt32() { writer.writeInt32("int32", 32); - assertThat(buffer).isEqualToIgnoringWhitespace("'int32':{'$numberInt':32}"); + assertThat(buffer).isEqualToIgnoringWhitespace("'int32':{'$numberInt':'32'}"); } @Test @@ -73,7 +73,7 @@ void writeInt64() { writer.writeInt64("int64", 64); - assertThat(buffer).isEqualToIgnoringWhitespace("'int64':{'$numberLong':64}"); + assertThat(buffer).isEqualToIgnoringWhitespace("'int64':{'$numberLong':'64'}"); } @Test @@ -81,7 +81,7 @@ void writeDouble() { writer.writeDouble("double", 42.24D); - assertThat(buffer).isEqualToIgnoringWhitespace("'double':{'$numberDouble':42.24}"); + assertThat(buffer).isEqualToIgnoringWhitespace("'double':{'$numberDouble':'42.24'}"); } @Test @@ -89,7 +89,7 @@ void writeDecimal128() { writer.writeDecimal128("decimal128", new Decimal128(128L)); - assertThat(buffer).isEqualToIgnoringWhitespace("'decimal128':{'$numberDecimal':128}"); + assertThat(buffer).isEqualToIgnoringWhitespace("'decimal128':{'$numberDecimal':'128'}"); } @Test @@ -142,7 +142,7 @@ void writeArrayWithSingleEntry() { writer.writeInt32(42); writer.writeEndArray(); - assertThat(buffer).isEqualToIgnoringNewLines("[{'$numberInt':42}]"); + assertThat(buffer).isEqualToIgnoringNewLines("[{'$numberInt':'42'}]"); } @Test @@ -153,7 +153,7 @@ void writeArrayWithMultipleEntries() { writer.writeInt64(24); writer.writeEndArray(); - assertThat(buffer).isEqualToIgnoringNewLines("[{'$numberInt':42},{'$numberLong':24}]"); + assertThat(buffer).isEqualToIgnoringNewLines("[{'$numberInt':'42'},{'$numberLong':'24'}]"); } } From fac45f852c925dab0987d466846ce31cef9c83a1 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 16 Jan 2025 10:59:34 +0100 Subject: [PATCH 09/13] Initial test support for generated repositories. --- .../test/java/example/aot/UserRepository.java | 7 +- .../MongoRepositoryContributorTests.java | 254 +++++++++++++++++- .../mongodb/test/util/MongoTestTemplate.java | 10 + 3 files changed, 260 insertions(+), 11 deletions(-) diff --git a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java index 4e89085b14..0b76fb0a3e 100644 --- a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java +++ b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java @@ -16,6 +16,7 @@ package example.aot; import java.util.List; +import java.util.Optional; import org.springframework.data.domain.Limit; import org.springframework.data.domain.Page; @@ -32,6 +33,9 @@ */ public interface UserRepository extends CrudRepository { + User findOneByUsername(String username); + + Optional findOptionalOneByUsername(String username); @Query("{ 'username' : '?0' }") List findAllByAnnotatedQueryWithParameter(String username); @@ -42,8 +46,9 @@ public interface UserRepository extends CrudRepository { }""") List findAllByAnnotatedMultilineQueryWithParameter(String username); + @ReadPreference("secondary") - User findByUsername(String username); + User findWithReadPreferenceByUsername(String username); Page findUserProjectionBy(Pageable pageable); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java index a24707531f..59d659c4b4 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java @@ -32,29 +32,263 @@ package org.springframework.data.mongodb.aot.generated; import static org.assertj.core.api.Assertions.assertThat; + +import example.aot.User; import example.aot.UserRepository; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.bson.Document; +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.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.core.test.tools.TestCompiler; +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.data.mongodb.test.util.MongoTestUtils; +import org.springframework.data.util.Lazy; +import org.springframework.test.util.ReflectionTestUtils; + +import com.mongodb.client.MongoClient; /** * @author Christoph Strobl * @since 2025/01 */ +@ExtendWith(MongoClientExtension.class) public class MongoRepositoryContributorTests { - @Test - public void testCompile() { + private static final String DB_NAME = "aot-repo-tests"; + private static Verifyer generatedContext; + + @Client static MongoClient client; + + @BeforeAll + static void beforeAll() { + + TestMongoAotRepositoryContext aotContext = new TestMongoAotRepositoryContext(UserRepository.class, null); + TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class); + + new MongoRepositoryContributor(aotContext).contribute(generationContext); + + AbstractBeanDefinition mongoTemplate = BeanDefinitionBuilder.rootBeanDefinition(MongoTestTemplate.class) + .addConstructorArgValue(DB_NAME).getBeanDefinition(); + AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder + .genericBeanDefinition("example.aot.UserRepositoryImpl__Aot").addConstructorArgReference("mongoOperations") + .getBeanDefinition(); + + generatedContext = generateContext(generationContext) // + .register("mongoOperations", mongoTemplate) // + .register("aotUserRepository", aotGeneratedRepository); + } + + @BeforeEach + void beforeEach() { + + MongoTestUtils.flushCollection(DB_NAME, "user", client); + + initUsers(); + } + + @Test + void testFindDerivedFinderSingleEntity() { + + generatedContext.verify(methodInvoker -> { + + User user = methodInvoker.invoke("findOneByUsername", "yoda").onBean("aotUserRepository"); + assertThat(user).isNotNull().extracting(User::getUsername).isEqualTo("yoda"); + }); + + } + + @Test + void testCountWorksAsExpected() { + + generatedContext.verify(methodInvoker -> { + + Long value = methodInvoker.invoke("countUsersByLastnameLike", "Sky").onBean("aotUserRepository"); + assertThat(value).isEqualTo(2L); + }); + } + + private static void initUsers() { + + Document luke = Document.parse(""" + { + "_id": "id-1", + "username": "luke", + "first_name": "Luke", + "last_name": "Skywalker", + "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", + "_class": "example.springdata.aot.User" + }"""); + + Document yoda = Document.parse( + """ + { + "_id": "id-5", + "username": "yoda", + "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", + "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)); + } + + static GeneratedContextBuilder generateContext(TestGenerationContext generationContext) { + return new GeneratedContextBuilder(generationContext); + } + + static class GeneratedContextBuilder implements Verifyer { + + TestGenerationContext generationContext; + Map beanDefinitions = new LinkedHashMap<>(); + Lazy lazyFactory; + + public GeneratedContextBuilder(TestGenerationContext generationContext) { + + this.generationContext = generationContext; + this.lazyFactory = Lazy.of(() -> { + DefaultListableBeanFactory freshBeanFactory = new DefaultListableBeanFactory(); + TestCompiler.forSystem().with(generationContext).compile(compiled -> { + + freshBeanFactory.setBeanClassLoader(compiled.getClassLoader()); + for (Entry entry : beanDefinitions.entrySet()) { + freshBeanFactory.registerBeanDefinition(entry.getKey(), entry.getValue()); + } + }); + return freshBeanFactory; + }); + } + + GeneratedContextBuilder register(String name, BeanDefinition beanDefinition) { + this.beanDefinitions.put(name, beanDefinition); + return this; + } + + public Verifyer verify(Consumer methodInvoker) { + methodInvoker.accept(new GeneratedContext(lazyFactory)); + return this; + } + + } + + interface Verifyer { + Verifyer verify(Consumer methodInvoker); + } + + static class GeneratedContext { + + private Supplier delegate; + + public GeneratedContext(Supplier defaultListableBeanFactory) { + this.delegate = defaultListableBeanFactory; + } - TestMongoAotRepositoryContext aotContext = new TestMongoAotRepositoryContext(UserRepository.class, null); - TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class); + InvocationBuilder invoke(String method, Object... arguments) { - new MongoRepositoryContributor(aotContext).contribute(generationContext); - generationContext.writeGeneratedContent(); + return new InvocationBuilder() { + @Override + public T onBean(String beanName) { + Object bean = delegate.get().getBean(beanName); + return ReflectionTestUtils.invokeMethod(bean, method, arguments); + } + }; + } - TestCompiler.forSystem().with(generationContext).compile(compiled -> { - assertThat(compiled.getAllCompiledClasses()).map(Class::getName).contains("example.aot.UserRepositoryImpl__Aot"); - }); - } + interface InvocationBuilder { + T onBean(String beanName); + } + } } 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 8e837b2599..398e77594a 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 @@ -20,11 +20,13 @@ 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.core.MongoTemplateTests; import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; import com.mongodb.MongoWriteException; @@ -41,6 +43,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 -> { From 428ec8aa7fad458a95b2e1155fadd4b234e8fd7d Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 16 Jan 2025 16:49:59 +0100 Subject: [PATCH 10/13] Add more tests --- .../test/java/example/aot/UserRepository.java | 12 ++-- .../MongoRepositoryContributorTests.java | 64 +++++++++++++++++-- 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java index 0b76fb0a3e..02492a7c35 100644 --- a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java +++ b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java @@ -33,10 +33,18 @@ */ public interface UserRepository extends CrudRepository { + List findUserNoArgumentsBy(); + User findOneByUsername(String username); Optional findOptionalOneByUsername(String username); + Long countUsersByLastname(String lastname); + + Boolean existsUserByLastname(String lastname); + + List findTop2ByLastnameStartingWith(String lastname); + @Query("{ 'username' : '?0' }") List findAllByAnnotatedQueryWithParameter(String username); @@ -55,15 +63,11 @@ public interface UserRepository extends CrudRepository { @Query(sort = "{ 'last_name' : -1}") List findByLastnameAfter(String lastname); - Long countUsersByLastnameLike(String lastname); - Boolean existsUserByLastname(String lastname); @Query(fields = "{ '_id' : -1}") List findByLastnameBefore(String lastname); - List findTop5ByUsernameLike(String username); - List findByLastnameOrderByFirstnameDesc(String lastname); List findUserByLastnameLike(String lastname); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java index 59d659c4b4..f0cf6750a1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java @@ -40,6 +40,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.function.Consumer; import java.util.function.Supplier; @@ -71,7 +72,7 @@ public class MongoRepositoryContributorTests { private static final String DB_NAME = "aot-repo-tests"; - private static Verifyer generatedContext; + private static Verifyer generated; @Client static MongoClient client; @@ -89,7 +90,7 @@ static void beforeAll() { .genericBeanDefinition("example.aot.UserRepositoryImpl__Aot").addConstructorArgReference("mongoOperations") .getBeanDefinition(); - generatedContext = generateContext(generationContext) // + generated = generateContext(generationContext) // .register("mongoOperations", mongoTemplate) // .register("aotUserRepository", aotGeneratedRepository); } @@ -98,31 +99,82 @@ static void beforeAll() { void beforeEach() { MongoTestUtils.flushCollection(DB_NAME, "user", client); - initUsers(); } @Test void testFindDerivedFinderSingleEntity() { - generatedContext.verify(methodInvoker -> { + generated.verify(methodInvoker -> { User user = methodInvoker.invoke("findOneByUsername", "yoda").onBean("aotUserRepository"); assertThat(user).isNotNull().extracting(User::getUsername).isEqualTo("yoda"); }); + } + + @Test + void testFindDerivedFinderOptionalEntity() { + + generated.verify(methodInvoker -> { + + Optional user = methodInvoker.invoke("findOptionalOneByUsername", "yoda").onBean("aotUserRepository"); + assertThat(user).isNotNull().containsInstanceOf(User.class) + .hasValueSatisfying(it -> assertThat(it).extracting(User::getUsername).isEqualTo("yoda")); + }); + } + + @Test + void testDerivedCount() { + + generated.verify(methodInvoker -> { + + Long value = methodInvoker.invoke("countUsersByLastname", "Skywalker").onBean("aotUserRepository"); + assertThat(value).isEqualTo(2L); + }); + } + + @Test + void testDerivedExists() { + + generated.verify(methodInvoker -> { + + Boolean exists = methodInvoker.invoke("existsUserByLastname", "Skywalker").onBean("aotUserRepository"); + assertThat(exists).isTrue(); + }); + } + + @Test + void testDerivedFinderWithoutArguments() { + + generated.verify(methodInvoker -> { + List users = methodInvoker.invoke("findUserNoArgumentsBy").onBean("aotUserRepository"); + assertThat(users).hasSize(7).hasOnlyElementsOfType(User.class); + }); } @Test void testCountWorksAsExpected() { - generatedContext.verify(methodInvoker -> { + generated.verify(methodInvoker -> { - Long value = methodInvoker.invoke("countUsersByLastnameLike", "Sky").onBean("aotUserRepository"); + Long value = methodInvoker.invoke("countUsersByLastname", "Skywalker").onBean("aotUserRepository"); assertThat(value).isEqualTo(2L); }); } + @Test + void testLimitedDerivedFinder() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findTop2ByLastnameStartingWith", "S").onBean("aotUserRepository"); + assertThat(users).hasSize(2); + }); + } + + // countUsersByLastname + private static void initUsers() { Document luke = Document.parse(""" From 042ab7efd5b96cfc342b7a5ed1c1e89e626a2ebd Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 17 Jan 2025 10:36:17 +0100 Subject: [PATCH 11/13] And moooore tests --- .../test/java/example/aot/UserRepository.java | 85 +++++--- .../MongoRepositoryContributorTests.java | 205 ++++++++++++++++++ 2 files changed, 262 insertions(+), 28 deletions(-) diff --git a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java index 02492a7c35..cddfbe1e8f 100644 --- a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java +++ b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java @@ -33,50 +33,79 @@ */ public interface UserRepository extends CrudRepository { - List findUserNoArgumentsBy(); + List findUserNoArgumentsBy(); - User findOneByUsername(String username); + User findOneByUsername(String username); - Optional findOptionalOneByUsername(String username); + Optional findOptionalOneByUsername(String username); - Long countUsersByLastname(String lastname); + Long countUsersByLastname(String lastname); - Boolean existsUserByLastname(String lastname); + Boolean existsUserByLastname(String lastname); - List findTop2ByLastnameStartingWith(String lastname); + List findByLastnameStartingWith(String lastname); - @Query("{ 'username' : '?0' }") - List findAllByAnnotatedQueryWithParameter(String username); + List findTop2ByLastnameStartingWith(String lastname); - @Query(""" - { - 'username' : '?0' - }""") - List findAllByAnnotatedMultilineQueryWithParameter(String username); + List findByLastnameStartingWithOrderByUsername(String lastname); + List findByLastnameStartingWith(String lastname, Limit limit); - @ReadPreference("secondary") - User findWithReadPreferenceByUsername(String username); + List findByLastnameStartingWith(String lastname, Sort sort); - Page findUserProjectionBy(Pageable pageable); + List findByLastnameStartingWith(String lastname, Sort sort, Limit limit); - @Query(sort = "{ 'last_name' : -1}") - List findByLastnameAfter(String lastname); + List findByLastnameStartingWith(String lastname, Pageable page); + Page findPageOfUsersByLastnameStartingWith(String lastname, Pageable page); + Slice findSliceOfUserByLastnameStartingWith(String lastname, Pageable page); - @Query(fields = "{ '_id' : -1}") - List findByLastnameBefore(String lastname); + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + List findAnnotatedQueryByLastname(String lastname); - List findByLastnameOrderByFirstnameDesc(String lastname); + @Query(""" + { + 'lastname' : { + '$regex' : '^?0' + } + }""") + List findAnnotatedMultilineQueryByLastname(String username); - List findUserByLastnameLike(String lastname); + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + List findAnnotatedQueryByLastname(String lastname, Limit limit); - List findUserByLastnameStartingWith(String lastname, Pageable page); - List findUserByLastnameStartingWith(String lastname, Sort sort); - List findUserByLastnameStartingWith(String lastname, Limit limit); - List findUserByLastnameStartingWith(String lastname, Sort sort, 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); + + @Query(sort = "{ 'username' : 1 }") + List findWithAnnotatedSortByLastnameStartingWith(String lastname); + + @ReadPreference("secondary") + User findWithReadPreferenceByUsername(String username); + + Page findUserProjectionBy(Pageable pageable); + + @Query(sort = "{ 'last_name' : -1}") + List findByLastnameAfter(String lastname); + + @Query(fields = "{ '_id' : -1}") + List findByLastnameBefore(String lastname); + + List findByLastnameOrderByFirstnameDesc(String lastname); + + List findUserByLastnameLike(String lastname); - Page findUserByFirstnameStartingWith(String firstname, Pageable page); - Slice findUserByFirstnameLike(String firstname, Pageable page); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java index f0cf6750a1..fb7611e15a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java @@ -55,6 +55,11 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.core.test.tools.TestCompiler; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.test.util.Client; import org.springframework.data.mongodb.test.util.MongoClientExtension; import org.springframework.data.mongodb.test.util.MongoTestTemplate; @@ -163,6 +168,16 @@ void testCountWorksAsExpected() { }); } + @Test + void testDerivedFinderReturningList() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findByLastnameStartingWith", "S").onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("luke", "vader", "kylo", "han"); + }); + } + @Test void testLimitedDerivedFinder() { @@ -173,6 +188,196 @@ void testLimitedDerivedFinder() { }); } + @Test + void testSortedDerivedFinder() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findByLastnameStartingWithOrderByUsername", "S") + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); + }); + } + + @Test + void testDerivedFinderWithLimitArgument() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findByLastnameStartingWith", "S", Limit.of(2)) + .onBean("aotUserRepository"); + assertThat(users).hasSize(2); + }); + } + + @Test + void testDerivedFinderWithSort() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findByLastnameStartingWith", "S", Sort.by("username")) + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); + }); + } + + @Test + void testDerivedFinderWithSortAndLimit() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findByLastnameStartingWith", "S", Sort.by("username"), Limit.of(2)) + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo"); + }); + } + + @Test + void testDerivedFinderReturningListWithPageable() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker + .invoke("findByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("username"))) + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo"); + }); + } + + @Test + void testDerivedFinderReturningPage() { + + generated.verify(methodInvoker -> { + + Page page = methodInvoker + .invoke("findPageOfUsersByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("username"))) + .onBean("aotUserRepository"); + assertThat(page.getTotalElements()).isEqualTo(4); + assertThat(page.getSize()).isEqualTo(2); + assertThat(page.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); + }); + } + + @Test + void testDerivedFinderReturningSlice() { + + generated.verify(methodInvoker -> { + + Slice slice = methodInvoker + .invoke("findSliceOfUserByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("username"))) + .onBean("aotUserRepository"); + assertThat(slice.hasNext()).isTrue(); + assertThat(slice.getSize()).isEqualTo(2); + assertThat(slice.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); + }); + } + + @Test + void testAnnotatedFinderWithQuery() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S").onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader"); + }); + } + + @Test + void testAnnotatedMultilineFinderWithQuery() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findAnnotatedMultilineQueryByLastname", "S").onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader"); + }); + } + + @Test + void testAnnotatedFinderWithQueryAndLimit() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Limit.of(2)) + .onBean("aotUserRepository"); + assertThat(users).hasSize(2); + }); + } + + @Test + void testAnnotatedFinderWithQueryAndSort() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Sort.by("username")) + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); + }); + } + + @Test + void testAnnotatedFinderWithQueryLimitAndSort() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Limit.of(2), Sort.by("username")) + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo"); + }); + } + + @Test + void testAnnotatedFinderReturningListWithPageable() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker + .invoke("findAnnotatedQueryByLastname", "S", PageRequest.of(0, 2, Sort.by("username"))) + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo"); + }); + } + + @Test + void testAnnotatedFinderReturningPage() { + + generated.verify(methodInvoker -> { + + Page page = methodInvoker + .invoke("findAnnotatedQueryPageOfUsersByLastname", "S", PageRequest.of(0, 2, Sort.by("username"))) + .onBean("aotUserRepository"); + assertThat(page.getTotalElements()).isEqualTo(4); + assertThat(page.getSize()).isEqualTo(2); + assertThat(page.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); + }); + } + + @Test + void testAnnotatedFinderReturningSlice() { + + generated.verify(methodInvoker -> { + + Slice slice = methodInvoker + .invoke("findAnnotatedQuerySliceOfUsersByLastname", "S", PageRequest.of(0, 2, Sort.by("username"))) + .onBean("aotUserRepository"); + assertThat(slice.hasNext()).isTrue(); + assertThat(slice.getSize()).isEqualTo(2); + assertThat(slice.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); + }); + } + + @Test + void testDerivedFinderWithAnnotatedSort() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findWithAnnotatedSortByLastnameStartingWith", "S") + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); + }); + } + + // findAnnotatedQueryPageOfUsersByLastname + // countUsersByLastname private static void initUsers() { From 0aa54f2b9bcb339d23f037cc892c4d154df46b97 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 17 Jan 2025 11:36:08 +0100 Subject: [PATCH 12/13] regorganize stuff --- .../mongodb/aot/generated/MongoBlocks.java | 30 ++------ .../test/java/example/aot/UserRepository.java | 37 +++++++--- .../MongoRepositoryContributorTests.java | 73 ++++++++++++++++++- 3 files changed, 105 insertions(+), 35 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java index c4c4050802..653ec17b44 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java @@ -75,14 +75,9 @@ CodeBlock build(String queryVariableName) { builder.add("\n"); if (isProjecting) { - // builder.addStatement("$T<$T> finder = $L.query($T.class).as($T.class).matching($L)", TerminatingFind.class, - // actualReturnType, mongoOpsRef, repositoryInformation.getDomainType(), actualReturnType, queryVariableName); builder.addStatement("$T<$T> finder = $L.query($T.class).as($T.class)", FindWithQuery.class, actualReturnType, mongoOpsRef, context.getRepositoryInformation().getDomainType(), actualReturnType); } else { - // builder.addStatement("$T<$T> finder = $L.query($T.class).matching($L)", TerminatingFind.class, - // actualReturnType, - // mongoOpsRef, repositoryInformation.getDomainType(), queryVariableName); builder.addStatement("$T<$T> finder = $L.query($T.class)", FindWithQuery.class, actualReturnType, mongoOpsRef, context.getRepositoryInformation().getDomainType()); } @@ -92,9 +87,9 @@ CodeBlock build(String queryVariableName) { if (context.returnsOptionalValue()) { terminatingMethod = "one()"; - } else if(context.isCountMethod()){ + } else if (context.isCountMethod()) { terminatingMethod = "count()"; - } else if(context.isExistsMethod()){ + } else if (context.isExistsMethod()) { terminatingMethod = "exists()"; } else { terminatingMethod = "oneValue()"; @@ -110,13 +105,8 @@ CodeBlock build(String queryVariableName) { context.getPageableParameterName(), queryVariableName); } else { builder.addStatement("return finder.matching($L).$L", queryVariableName, terminatingMethod); - // builder.addStatement("return $T.getPage(finder.$L, $L, () -> finder.count())", PageableExecutionUtils.class, - // terminatingMethod, - // metadata.getPageableParameterName()); } - // new MongoQueryExecution.PagedExecution(finder, page).execute(query); - return builder.build(); } @@ -128,9 +118,7 @@ static class QueryBlockBuilder { AotRepositoryMethodGenerationContext context; StringQuery source; List arguments; - - // MongoParameters argumentSource; - + public QueryBlockBuilder(AotRepositoryMethodGenerationContext context) { this.context = context; this.arguments = Arrays.stream(context.getMethod().getParameters()).map(Parameter::getName) @@ -151,8 +139,6 @@ CodeBlock build(String queryVariableName) { CodeBlock.Builder builder = CodeBlock.builder(); builder.add("\n"); - // String queryStringName = "%sString".formatted(queryVariableName); - // builder.addStatement("String $L = $S", queryStringName, queryString); String queryDocumentVariableName = "%sDocument".formatted(queryVariableName); builder.add(renderExpressionToDocument(source.getQueryString(), queryVariableName)); builder.addStatement("$T $L = new $T($L)", BasicQuery.class, queryVariableName, BasicQuery.class, @@ -181,7 +167,7 @@ CodeBlock build(String queryVariableName) { } String pageableParameter = context.getPageableParameterName(); - if (StringUtils.hasText(pageableParameter) && (!context.returnsPage() || !context.returnsSlice())) { + if (StringUtils.hasText(pageableParameter) && !context.returnsPage() && !context.returnsSlice()) { builder.addStatement("$L.with($L)", queryVariableName, pageableParameter); } @@ -205,11 +191,9 @@ CodeBlock build(String queryVariableName) { private CodeBlock renderExpressionToDocument(@Nullable String source, String variableName) { Builder builder = CodeBlock.builder(); - if(!StringUtils.hasText(source)) { - builder.addStatement("$T $L = new $T()", Document.class, "%sDocument".formatted(variableName), - Document.class); - } - else if (!containsPlaceholder(source)) { + if (!StringUtils.hasText(source)) { + builder.addStatement("$T $L = new $T()", Document.class, "%sDocument".formatted(variableName), Document.class); + } else if (!containsPlaceholder(source)) { builder.addStatement("$T $L = $T.parse($S)", Document.class, "%sDocument".formatted(variableName), Document.class, source); } else { diff --git a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java index cddfbe1e8f..8a796e52f3 100644 --- a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java +++ b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java @@ -33,6 +33,8 @@ */ public interface UserRepository extends CrudRepository { + /* Derived Queries */ + List findUserNoArgumentsBy(); User findOneByUsername(String username); @@ -61,6 +63,18 @@ public interface UserRepository extends CrudRepository { Slice findSliceOfUserByLastnameStartingWith(String lastname, Pageable page); + // TODO: Streaming + // TODO: Scrolling + // TODO: GeoQueries + + /* 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); @@ -90,22 +104,27 @@ public interface UserRepository extends CrudRepository { @Query("{ 'lastname' : { '$regex' : '^?0' } }") Slice findAnnotatedQuerySliceOfUsersByLastname(String lastname, Pageable pageable); + // TODO: deletes + // TODO: updates + // TODO: Aggregations + + /* Derived With Annotated Options */ + @Query(sort = "{ 'username' : 1 }") List findWithAnnotatedSortByLastnameStartingWith(String lastname); - @ReadPreference("secondary") - User findWithReadPreferenceByUsername(String username); + @Query(fields = "{ 'username' : 1 }") + List findWithAnnotatedFieldsProjectionByLastnameStartingWith(String lastname); - Page findUserProjectionBy(Pageable pageable); + @ReadPreference("no-such-read-preference") + User findWithReadPreferenceByUsername(String username); - @Query(sort = "{ 'last_name' : -1}") - List findByLastnameAfter(String lastname); + // TODO: hints - @Query(fields = "{ '_id' : -1}") - List findByLastnameBefore(String lastname); + /* Projecting Queries */ - List findByLastnameOrderByFirstnameDesc(String lastname); + List findUserProjectionByLastnameStartingWith(String lastname); - List findUserByLastnameLike(String lastname); + Page findUserProjectionByLastnameStartingWith(String lastname, Pageable page); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java index fb7611e15a..4e0d8bb7c8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java @@ -32,8 +32,10 @@ package org.springframework.data.mongodb.aot.generated; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import example.aot.User; +import example.aot.UserProjection; import example.aot.UserRepository; import java.util.LinkedHashMap; @@ -66,6 +68,7 @@ import org.springframework.data.mongodb.test.util.MongoTestUtils; import org.springframework.data.util.Lazy; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.StringUtils; import com.mongodb.client.MongoClient; @@ -273,7 +276,27 @@ void testDerivedFinderReturningSlice() { } @Test - void testAnnotatedFinderWithQuery() { + void testAnnotatedFinderReturningSingleValueWithQuery() { + + generated.verify(methodInvoker -> { + + User user = methodInvoker.invoke("findAnnotatedQueryByUsername", "yoda").onBean("aotUserRepository"); + assertThat(user).isNotNull().extracting(User::getUsername).isEqualTo("yoda"); + }); + } + + @Test + void testAnnotatedCount() { + + generated.verify(methodInvoker -> { + + Long value = methodInvoker.invoke("countAnnotatedQueryByLastname", "Skywalker").onBean("aotUserRepository"); + assertThat(value).isEqualTo(2L); + }); + } + + @Test + void testAnnotatedFinderReturningListWithQuery() { generated.verify(methodInvoker -> { @@ -376,9 +399,53 @@ void testDerivedFinderWithAnnotatedSort() { }); } - // findAnnotatedQueryPageOfUsersByLastname + @Test + void testDerivedFinderWithAnnotatedFieldsProjection() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findWithAnnotatedFieldsProjectionByLastnameStartingWith", "S") + .onBean("aotUserRepository"); + assertThat(users).allMatch( + user -> StringUtils.hasText(user.getUsername()) && user.getLastname() == null && user.getFirstname() == null); + }); + } - // countUsersByLastname + @Test + void testReadPreferenceAppliedToQuery() { + + generated.verify(methodInvoker -> { + + // check if it fails when trying to parse the read preference to indicate it would get applied + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> methodInvoker.invoke("findWithReadPreferenceByUsername", "S").onBean("aotUserRepository")) + .withMessageContaining("No match for read preference"); + }); + } + + @Test + void testDerivedFinderReturningListOfProjections() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findUserProjectionByLastnameStartingWith", "S") + .onBean("aotUserRepository"); + assertThat(users).extracting(UserProjection::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", + "vader"); + }); + } + + @Test + void testDerivedFinderReturningPageOfProjections() { + + generated.verify(methodInvoker -> { + + Page users = methodInvoker + .invoke("findUserProjectionByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("username"))) + .onBean("aotUserRepository"); + assertThat(users).extracting(UserProjection::getUsername).containsExactly("han", "kylo"); + }); + } private static void initUsers() { From 7c34b5b1365c62dab8470d316e8985f4508844b7 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 17 Jan 2025 14:20:40 +0100 Subject: [PATCH 13/13] Support delete methods --- .../mongodb/aot/generated/MongoBlocks.java | 86 +++++++++++++++++-- .../generated/MongoRepositoryContributor.java | 20 +++-- .../repository/query/MongoQueryExecution.java | 37 ++++++++ .../test/java/example/aot/UserRepository.java | 18 +++- .../MongoRepositoryContributorTests.java | 44 ++++++++++ 5 files changed, 191 insertions(+), 14 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java index 653ec17b44..1f550d814e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java @@ -24,17 +24,22 @@ import org.bson.Document; import org.springframework.data.mongodb.BindableMongoExpression; import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; +import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.repository.Hint; import org.springframework.data.mongodb.repository.ReadPreference; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecutionX; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecutionX.Type; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagedExecution; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SlicedExecution; import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext; +import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; import org.springframework.javapoet.TypeName; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -45,23 +50,84 @@ public class MongoBlocks { private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)"); - public static QueryBlockBuilder queryBlockBuilder(AotRepositoryMethodGenerationContext context) { + static QueryBlockBuilder queryBlockBuilder(AotRepositoryMethodGenerationContext context) { return new QueryBlockBuilder(context); } - public static QueryExecutionBlockBuilder queryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { + static QueryExecutionBlockBuilder queryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { return new QueryExecutionBlockBuilder(context); } + static DeleteExecutionBuilder deleteExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { + return new DeleteExecutionBuilder(context); + } + + static class DeleteExecutionBuilder { + + AotRepositoryMethodGenerationContext context; + String queryVariableName; + + public DeleteExecutionBuilder(AotRepositoryMethodGenerationContext context) { + this.context = context; + } + + public DeleteExecutionBuilder referencing(String queryVariableName) { + this.queryVariableName = queryVariableName; + return this; + } + + public CodeBlock build() { + + String mongoOpsRef = context.fieldNameOf(MongoOperations.class); + Builder builder = CodeBlock.builder(); + + boolean isProjecting = context.getActualReturnType() != null + && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), + context.getActualReturnType()); + + Object actualReturnType = isProjecting ? context.getActualReturnType() + : context.getRepositoryInformation().getDomainType(); + + builder.add("\n"); + builder.addStatement("$T<$T> remover = $L.remove($T.class)", ExecutableRemove.class, actualReturnType, + mongoOpsRef, context.getRepositoryInformation().getDomainType()); + + Type type = Type.FIND_AND_REMOVE_ALL; + if (context.returnsSingleValue()) { + if (!ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType())) { + type = Type.FIND_AND_REMOVE_ONE; + } else { + type = Type.ALL; + } + } + + actualReturnType = ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType()) + ? ClassName.get(context.getMethod().getReturnType()) + : context.returnsSingleValue() ? actualReturnType : context.getReturnType(); + + builder.addStatement("return ($T) new $T(remover, $T.$L).execute($L)", actualReturnType, DeleteExecutionX.class, + DeleteExecutionX.Type.class, type.name(), queryVariableName); + + return builder.build(); + } + } + static class QueryExecutionBlockBuilder { AotRepositoryMethodGenerationContext context; + private String queryVariableName; public QueryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { this.context = context; } - CodeBlock build(String queryVariableName) { + QueryExecutionBlockBuilder referencing(String queryVariableName) { + + this.queryVariableName = queryVariableName; + return this; + } + + CodeBlock build() { String mongoOpsRef = context.fieldNameOf(MongoOperations.class); @@ -74,10 +140,12 @@ CodeBlock build(String queryVariableName) { : context.getRepositoryInformation().getDomainType(); builder.add("\n"); + if (isProjecting) { builder.addStatement("$T<$T> finder = $L.query($T.class).as($T.class)", FindWithQuery.class, actualReturnType, mongoOpsRef, context.getRepositoryInformation().getDomainType(), actualReturnType); } else { + builder.addStatement("$T<$T> finder = $L.query($T.class)", FindWithQuery.class, actualReturnType, mongoOpsRef, context.getRepositoryInformation().getDomainType()); } @@ -97,7 +165,6 @@ CodeBlock build(String queryVariableName) { } if (context.returnsPage()) { - // builder.addStatement("return finder.$L", terminatingMethod); builder.addStatement("return new $T(finder, $L).execute($L)", PagedExecution.class, context.getPageableParameterName(), queryVariableName); } else if (context.returnsSlice()) { @@ -110,7 +177,6 @@ CodeBlock build(String queryVariableName) { return builder.build(); } - } static class QueryBlockBuilder { @@ -118,7 +184,8 @@ static class QueryBlockBuilder { AotRepositoryMethodGenerationContext context; StringQuery source; List arguments; - + private String queryVariableName; + public QueryBlockBuilder(AotRepositoryMethodGenerationContext context) { this.context = context; this.arguments = Arrays.stream(context.getMethod().getParameters()).map(Parameter::getName) @@ -134,7 +201,12 @@ public QueryBlockBuilder filter(StringQuery query) { return this; } - CodeBlock build(String queryVariableName) { + public QueryBlockBuilder usingQueryVariableName(String queryVariableName) { + this.queryVariableName = queryVariableName; + return this; + } + + CodeBlock build() { CodeBlock.Builder builder = CodeBlock.builder(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java index 85d3bfa015..d42afd61bc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java @@ -18,6 +18,7 @@ import java.util.regex.Pattern; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.data.mongodb.aot.generated.MongoBlocks.QueryBlockBuilder; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.repository.Aggregation; import org.springframework.data.mongodb.repository.Query; @@ -55,10 +56,6 @@ protected AotRepositoryMethodBuilder contributeRepositoryMethod( // TODO: do not generate stuff for spel expressions - // skip currently unsupported Stuff. - if (generationContext.isDeleteMethod()) { - return null; - } if (AnnotatedElementUtils.hasAnnotation(generationContext.getMethod(), Aggregation.class)) { return null; } @@ -100,8 +97,19 @@ protected AotRepositoryMethodBuilder contributeRepositoryMethod( private static void writeStringQuery(AotRepositoryMethodGenerationContext context, Builder body, StringQuery query) { body.addCode(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); - body.addCode(MongoBlocks.queryBlockBuilder(context).filter(query).build("query")); - body.addCode(MongoBlocks.queryExecutionBlockBuilder(context).build("query")); + QueryBlockBuilder queryBlockBuilder = MongoBlocks.queryBlockBuilder(context).filter(query); + + if (context.isDeleteMethod()) { + + String deleteQueryVariableName = "deleteQuery"; + body.addCode(queryBlockBuilder.usingQueryVariableName(deleteQueryVariableName).build()); + body.addCode(MongoBlocks.deleteExecutionBlockBuilder(context).referencing(deleteQueryVariableName).build()); + } else { + + String filterQueryVariableName = "filterQuery"; + body.addCode(queryBlockBuilder.usingQueryVariableName(filterQueryVariableName).build()); + body.addCode(MongoBlocks.queryExecutionBlockBuilder(context).referencing(filterQueryVariableName).build()); + } } private static void userAnnotatedQuery(AotRepositoryMethodGenerationContext context, Builder body, Query query) { 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 f1cbff5889..60de945286 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,6 +15,7 @@ */ package org.springframework.data.mongodb.repository.query; +import java.util.Iterator; import java.util.List; import java.util.function.Supplier; @@ -31,6 +32,9 @@ 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.query.NearQuery; @@ -242,6 +246,39 @@ public Object execute(Query query) { } } + final class DeleteExecutionX implements MongoQueryExecution { + + ExecutableRemoveOperation.ExecutableRemove remove; + Type type; + + public DeleteExecutionX(ExecutableRemove remove, Type type) { + this.remove = remove; + this.type = type; + } + + @Nullable + @Override + public 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(); + } + + public enum Type { + FIND_AND_REMOVE_ONE, FIND_AND_REMOVE_ALL, ALL + } + } + /** * {@link MongoQueryExecution} removing documents matching the query. * diff --git a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java index 8a796e52f3..104fd8d08e 100644 --- a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java +++ b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java @@ -104,7 +104,23 @@ public interface UserRepository extends CrudRepository { @Query("{ 'lastname' : { '$regex' : '^?0' } }") Slice findAnnotatedQuerySliceOfUsersByLastname(String lastname, Pageable pageable); - // TODO: deletes + /* 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); + // TODO: updates // TODO: Aggregations diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java index 4e0d8bb7c8..9caf74f31c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java @@ -51,6 +51,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; @@ -388,6 +390,48 @@ void testAnnotatedFinderReturningSlice() { }); } + @ParameterizedTest + @ValueSource(strings = { "deleteByUsername", "deleteAnnotatedQueryByUsername" }) + void testDeleteSingle(String methodName) { + + generated.verify(methodInvoker -> { + + User result = methodInvoker.invoke(methodName, "yoda").onBean("aotUserRepository"); + + assertThat(result).isNotNull().extracting(User::getUsername).isEqualTo("yoda"); + }); + + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(6L); + } + + @ParameterizedTest + @ValueSource(strings = { "deleteByLastnameStartingWith", "deleteAnnotatedQueryByLastnameStartingWith" }) + void testDerivedDeleteMultipleReturningDeleteCount(String methodName) { + + generated.verify(methodInvoker -> { + + Long result = methodInvoker.invoke(methodName, "S").onBean("aotUserRepository"); + + assertThat(result).isEqualTo(4L); + }); + + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L); + } + + @ParameterizedTest + @ValueSource(strings = { "deleteUsersByLastnameStartingWith", "deleteUsersAnnotatedQueryByLastnameStartingWith" }) + void testDerivedDeleteMultipleReturningDeleted(String methodName) { + + generated.verify(methodInvoker -> { + + List result = methodInvoker.invoke(methodName, "S").onBean("aotUserRepository"); + + assertThat(result).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader"); + }); + + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L); + } + @Test void testDerivedFinderWithAnnotatedSort() {