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..cdbb32b347 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 @@ -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/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 new file mode 100644 index 0000000000..1f550d814e --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java @@ -0,0 +1,290 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.aot.generated; + +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.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; + +/** + * @author Christoph Strobl + */ +public class MongoBlocks { + + private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)"); + + static QueryBlockBuilder queryBlockBuilder(AotRepositoryMethodGenerationContext context) { + return new QueryBlockBuilder(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; + } + + QueryExecutionBlockBuilder referencing(String queryVariableName) { + + this.queryVariableName = queryVariableName; + return this; + } + + 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"); + + 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()); + } + + 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()"; + } + } + + if (context.returnsPage()) { + builder.addStatement("return new $T(finder, $L).execute($L)", PagedExecution.class, + context.getPageableParameterName(), queryVariableName); + } else if (context.returnsSlice()) { + builder.addStatement("return new $T(finder, $L).execute($L)", SlicedExecution.class, + context.getPageableParameterName(), queryVariableName); + } else { + builder.addStatement("return finder.matching($L).$L", queryVariableName, terminatingMethod); + } + + return builder.build(); + + } + } + + 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) + .collect(Collectors.toList()); + + // ParametersSource parametersSource = ParametersSource.of(repositoryInformation, metadata.getRepositoryMethod()); + // this.argumentSource = new MongoParameters(parametersSource, false); + + } + + public QueryBlockBuilder filter(StringQuery query) { + this.source = query; + return this; + } + + public QueryBlockBuilder usingQueryVariableName(String queryVariableName) { + this.queryVariableName = queryVariableName; + return this; + } + + CodeBlock build() { + + CodeBlock.Builder builder = CodeBlock.builder(); + + builder.add("\n"); + 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) && !context.returnsPage() && !context.returnsSlice()) { + builder.addStatement("$L.with($L)", queryVariableName, pageableParameter); + } + + String hint = context.annotationValue(Hint.class, "value"); + + if (StringUtils.hasText(hint)) { + builder.addStatement("$L.withHint($S)", queryVariableName, hint); + } + + String readPreference = context.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(); + } + + 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 new file mode 100644 index 0000000000..d42afd61bc --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java @@ -0,0 +1,118 @@ +/* + * 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.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; +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.javapoet.MethodSpec.Builder; +import org.springframework.javapoet.TypeName; +import org.springframework.util.StringUtils; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public class MongoRepositoryContributor extends RepositoryContributor { + + private AotQueryCreator queryCreator; + + public MongoRepositoryContributor(AotRepositoryContext repositoryContext) { + super(repositoryContext); + this.queryCreator = new AotQueryCreator(); + } + + @Override + protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) { + constructorBuilder.addParameter("operations", TypeName.get(MongoOperations.class)); + } + + @Override + protected AotRepositoryMethodBuilder contributeRepositoryMethod( + AotRepositoryMethodGenerationContext generationContext) { + + // TODO: do not generate stuff for spel expressions + + 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 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()); + 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, StringQuery query) { + + body.addCode(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); + 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) { + 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/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..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 @@ -15,8 +15,9 @@ */ package org.springframework.data.mongodb.core.query; -import static org.springframework.data.mongodb.core.query.SerializationUtils.*; -import static org.springframework.util.ObjectUtils.*; +import static org.springframework.data.mongodb.core.query.SerializationUtils.serializeToJsonSafely; +import static org.springframework.util.ObjectUtils.nullSafeEquals; +import static org.springframework.util.ObjectUtils.nullSafeHashCode; import java.time.Duration; import java.util.ArrayList; 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..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 @@ -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,11 @@ 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/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/repository/query/MongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java index cebdf4e408..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; @@ -54,7 +58,7 @@ * @author Christoph Strobl */ @FunctionalInterface -interface MongoQueryExecution { +public interface MongoQueryExecution { @Nullable Object execute(Query query); @@ -66,12 +70,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 +86,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 +96,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 +107,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 +122,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); @@ -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/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/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/json/SpringJsonWriter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/SpringJsonWriter.java new file mode 100644 index 0000000000..370a272f53 --- /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"); + writeString(Double.valueOf(value).toString()); + writeEndDocument(); + } + + @Override + public void writeDouble(String name, double value) { + + writeName(name); + writeDouble(value); + } + + @Override + public void writeEndArray() { + write("]"); + context = context.parentContext; + if (context.contextType == JsonContextType.TOP_LEVEL) { + state = State.DONE; + } else { + setNextState(); + } + } + + @Override + public void writeEndDocument() { + buffer.append("}"); + context = context.parentContext; + if (context.contextType == JsonContextType.TOP_LEVEL) { + state = State.DONE; + } else { + setNextState(); + } + } + + @Override + public void writeInt32(int value) { + + writeStartDocument(); + writeName("$numberInt"); + writeString(Integer.valueOf(value).toString()); + writeEndDocument(); + } + + @Override + public void writeInt32(String name, int value) { + + writeName(name); + writeInt32(value); + } + + @Override + public void writeInt64(long value) { + + writeStartDocument(); + writeName("$numberLong"); + writeString(Long.valueOf(value).toString()); + writeEndDocument(); + } + + @Override + public void writeInt64(String name, long value) { + + writeName(name); + writeInt64(value); + } + + @Override + public void writeDecimal128(Decimal128 value) { + + // { "$numberDecimal": "" } + writeStartDocument(); + writeName("$numberDecimal"); + writeString(value.toString()); + writeEndDocument(); + } + + @Override + public void writeDecimal128(String name, Decimal128 value) { + + writeName(name); + writeDecimal128(value); + } + + @Override + public void writeJavaScript(String code) { + + writeStartDocument(); + writeName("$code"); + writeString(code); + writeEndDocument(); + } + + @Override + public void writeJavaScript(String name, String code) { + + writeName(name); + writeJavaScript(code); + } + + @Override + public void writeJavaScriptWithScope(String code) { + + } + + @Override + public void writeJavaScriptWithScope(String name, String code) { + + } + + @Override + public void writeMaxKey() { + + writeStartDocument(); + writeName("$maxKey"); + buffer.append(1); + writeEndDocument(); + } + + @Override + public void writeMaxKey(String name) { + writeName(name); + writeMaxKey(); + } + + @Override + public void writeMinKey() { + + writeStartDocument(); + writeName("$minKey"); + buffer.append(1); + writeEndDocument(); + } + + @Override + public void writeMinKey(String name) { + writeName(name); + writeMinKey(); + } + + @Override + public void writeName(String name) { + if (context.hasElements) { + write(","); + } else { + context.hasElements = true; + } + + writeString(name); + buffer.append(":"); + state = State.VALUE; + } + + @Override + public void writeNull() { + buffer.append("null"); + } + + @Override + public void writeNull(String name) { + writeName(name); + writeNull(); + } + + @Override + public void writeObjectId(ObjectId objectId) { + writeStartDocument(); + writeName("$oid"); + writeString(objectId.toHexString()); + writeEndDocument(); + } + + @Override + public void writeObjectId(String name, ObjectId objectId) { + writeName(name); + writeObjectId(objectId); + } + + @Override + public void writeRegularExpression(BsonRegularExpression regularExpression) { + + writeStartDocument(); + writeName("$regex"); + + write("/"); + write(regularExpression.getPattern()); + write("/"); + + if (StringUtils.hasText(regularExpression.getOptions())) { + writeName("$options"); + writeString(regularExpression.getOptions()); + } + + writeEndDocument(); + } + + @Override + public void writeRegularExpression(String name, BsonRegularExpression regularExpression) { + writeName(name); + writeRegularExpression(regularExpression); + } + + @Override + public void writeStartArray() { + + preWriteValue(); + write("["); + context = context.nestedArray(); + } + + @Override + public void writeStartArray(String name) { + writeName(name); + writeStartArray(); + } + + @Override + public void writeStartDocument() { + + preWriteValue(); + write("{"); + context = context.nestedDocument(); + state = State.NAME; + } + + @Override + public void writeStartDocument(String name) { + writeName(name); + writeStartDocument(); + } + + @Override + public void writeString(String value) { + write("'"); + write(value); + write("'"); + } + + @Override + public void writeString(String name, String value) { + writeName(name); + writeString(value); + } + + @Override + public void writeSymbol(String value) { + + writeStartDocument(); + writeName("$symbol"); + writeString(value); + writeEndDocument(); + } + + @Override + public void writeSymbol(String name, String value) { + + writeName(name); + writeSymbol(value); + } + + @Override // {"$timestamp": {"t": , "i": }} + public void writeTimestamp(BsonTimestamp value) { + + preWriteValue(); + writeStartDocument(); + writeName("$timestamp"); + writeStartDocument(); + writeName("t"); + buffer.append(value.getTime()); + writeName("i"); + buffer.append(value.getInc()); + writeEndDocument(); + writeEndDocument(); + } + + @Override + public void writeTimestamp(String name, BsonTimestamp value) { + + writeName(name); + writeTimestamp(value); + } + + @Override + public void writeUndefined() { + + writeStartDocument(); + writeName("$undefined"); + writeBoolean(true); + writeEndDocument(); + } + + @Override + public void writeUndefined(String name) { + + writeName(name); + writeUndefined(); + } + + @Override + public void pipe(BsonReader reader) { + + } + + 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/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/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 new file mode 100644 index 0000000000..104fd8d08e --- /dev/null +++ b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java @@ -0,0 +1,146 @@ +/* + * 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 java.util.Optional; + +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; +import org.springframework.data.repository.CrudRepository; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public interface UserRepository extends CrudRepository { + + /* Derived Queries */ + + List findUserNoArgumentsBy(); + + User findOneByUsername(String username); + + Optional findOptionalOneByUsername(String username); + + Long countUsersByLastname(String lastname); + + Boolean existsUserByLastname(String lastname); + + List findByLastnameStartingWith(String lastname); + + List findTop2ByLastnameStartingWith(String lastname); + + List findByLastnameStartingWithOrderByUsername(String lastname); + + List findByLastnameStartingWith(String lastname, Limit limit); + + List findByLastnameStartingWith(String lastname, Sort sort); + + List findByLastnameStartingWith(String lastname, Sort sort, Limit limit); + + List findByLastnameStartingWith(String lastname, Pageable page); + + Page findPageOfUsersByLastnameStartingWith(String lastname, Pageable page); + + Slice findSliceOfUserByLastnameStartingWith(String lastname, Pageable page); + + // 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); + + @Query(""" + { + 'lastname' : { + '$regex' : '^?0' + } + }""") + List findAnnotatedMultilineQueryByLastname(String username); + + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + List findAnnotatedQueryByLastname(String lastname, Limit limit); + + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + List findAnnotatedQueryByLastname(String lastname, Sort sort); + + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + List findAnnotatedQueryByLastname(String lastname, Limit limit, Sort sort); + + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + List findAnnotatedQueryByLastname(String lastname, Pageable pageable); + + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + Page findAnnotatedQueryPageOfUsersByLastname(String lastname, Pageable pageable); + + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + Slice findAnnotatedQuerySliceOfUsersByLastname(String lastname, Pageable pageable); + + /* deletes */ + + User deleteByUsername(String username); + + @Query(value = "{ 'username' : ?0 }", delete = true) + User deleteAnnotatedQueryByUsername(String username); + + Long deleteByLastnameStartingWith(String lastname); + + @Query(value = "{ 'lastname' : { '$regex' : '^?0' } }", delete = true) + Long deleteAnnotatedQueryByLastnameStartingWith(String lastname); + + List deleteUsersByLastnameStartingWith(String lastname); + + @Query(value = "{ 'lastname' : { '$regex' : '^?0' } }", delete = true) + List deleteUsersAnnotatedQueryByLastnameStartingWith(String lastname); + + // TODO: updates + // TODO: Aggregations + + /* Derived With Annotated Options */ + + @Query(sort = "{ 'username' : 1 }") + List findWithAnnotatedSortByLastnameStartingWith(String lastname); + + @Query(fields = "{ 'username' : 1 }") + List findWithAnnotatedFieldsProjectionByLastnameStartingWith(String lastname); + + @ReadPreference("no-such-read-preference") + User findWithReadPreferenceByUsername(String username); + + // TODO: hints + + /* Projecting Queries */ + + List findUserProjectionByLastnameStartingWith(String lastname); + + Page findUserProjectionByLastnameStartingWith(String lastname, Pageable page); + +} 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..9caf74f31c --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java @@ -0,0 +1,662 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import example.aot.User; +import example.aot.UserProjection; +import example.aot.UserRepository; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.function.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.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; +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; +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; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +@ExtendWith(MongoClientExtension.class) +public class MongoRepositoryContributorTests { + + private static final String DB_NAME = "aot-repo-tests"; + private static Verifyer generated; + + @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(); + + generated = generateContext(generationContext) // + .register("mongoOperations", mongoTemplate) // + .register("aotUserRepository", aotGeneratedRepository); + } + + @BeforeEach + void beforeEach() { + + MongoTestUtils.flushCollection(DB_NAME, "user", client); + initUsers(); + } + + @Test + void testFindDerivedFinderSingleEntity() { + + 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() { + + generated.verify(methodInvoker -> { + + Long value = methodInvoker.invoke("countUsersByLastname", "Skywalker").onBean("aotUserRepository"); + assertThat(value).isEqualTo(2L); + }); + } + + @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() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findTop2ByLastnameStartingWith", "S").onBean("aotUserRepository"); + assertThat(users).hasSize(2); + }); + } + + @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 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 -> { + + 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"); + }); + } + + @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() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findWithAnnotatedSortByLastnameStartingWith", "S") + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); + }); + } + + @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); + }); + } + + @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() { + + 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; + } + + InvocationBuilder invoke(String method, Object... arguments) { + + return new InvocationBuilder() { + @Override + public T onBean(String beanName) { + Object bean = delegate.get().getBean(beanName); + return ReflectionTestUtils.invokeMethod(bean, method, arguments); + } + }; + } + + interface InvocationBuilder { + T onBean(String beanName); + } + + } +} 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/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 -> { 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..57b8df548d --- /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'}]"); + } + +} 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 @@ +