Browse Source

Add support for MongoDB AOT Repositories.

Initial Support for generating repository source code at build time.

Closes: #4939
pull/4976/head
Christoph Strobl 11 months ago committed by Mark Paluch
parent
commit
69f50c6f72
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 6
      spring-data-mongodb/pom.xml
  2. 199
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/AotQueryCreator.java
  3. 290
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java
  4. 118
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java
  5. 227
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/StringQuery.java
  6. 15
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java
  7. 17
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java
  8. 14
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotMongoRepositoryPostProcessor.java
  9. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java
  10. 59
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java
  11. 3
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CrudMethodMetadataPostProcessor.java
  12. 67
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java
  13. 478
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/SpringJsonWriter.java
  14. 102
      spring-data-mongodb/src/test/java/example/aot/User.java
  15. 29
      spring-data-mongodb/src/test/java/example/aot/UserProjection.java
  16. 146
      spring-data-mongodb/src/test/java/example/aot/UserRepository.java
  17. 62
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/DemoRepo.java
  18. 662
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java
  19. 144
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/StubRepositoryInformation.java
  20. 129
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/TestMongoAotRepositoryContext.java
  21. 9
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java
  22. 159
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/SpringJsonWriterUnitTests.java
  23. 1
      spring-data-mongodb/src/test/resources/logback.xml

6
spring-data-mongodb/pom.xml

@ -288,6 +288,12 @@ @@ -288,6 +288,12 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Kotlin extension -->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>

199
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/AotQueryCreator.java

@ -0,0 +1,199 @@ @@ -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<Object> {
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<Placeholder> 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<Distance> 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<Object> iterator() {
return ((List) placeholders).iterator();
}
}
}

290
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java

@ -0,0 +1,290 @@ @@ -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<String> 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();
}
}
}

118
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java

@ -0,0 +1,118 @@ @@ -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()));
}
}

227
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/StringQuery.java

@ -0,0 +1,227 @@ @@ -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<Class<?>> 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<Collation> 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
}
}

15
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java

@ -29,9 +29,21 @@ import java.util.Map.Entry; @@ -29,9 +29,21 @@ import java.util.Map.Entry;
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.jspecify.annotations.Nullable;
import org.springframework.data.domain.Example;
@ -944,7 +956,8 @@ public class Criteria implements CriteriaDefinition { @@ -944,7 +956,8 @@ public class Criteria implements CriteriaDefinition {
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;

17
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java

@ -40,4 +40,21 @@ public interface CriteriaDefinition { @@ -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();
}
}
}

14
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotMongoRepositoryPostProcessor.java

@ -16,8 +16,11 @@ @@ -16,8 +16,11 @@
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.aot.generate.RepositoryContributor;
import org.springframework.data.repository.config.AotRepositoryContext;
import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor;
import org.springframework.data.util.TypeContributor;
@ -31,7 +34,8 @@ public class AotMongoRepositoryPostProcessor extends RepositoryRegistrationAotPr @@ -31,7 +34,8 @@ public class AotMongoRepositoryPostProcessor extends RepositoryRegistrationAotPr
private final LazyLoadingProxyAotProcessor lazyLoadingProxyAotProcessor = new LazyLoadingProxyAotProcessor();
@Override
protected void contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) {
protected RepositoryContributor contribute(AotRepositoryContext repositoryContext,
GenerationContext generationContext) {
// do some custom type registration here
super.contribute(repositoryContext, generationContext);
@ -39,6 +43,14 @@ public class AotMongoRepositoryPostProcessor extends RepositoryRegistrationAotPr @@ -39,6 +43,14 @@ public class AotMongoRepositoryPostProcessor extends RepositoryRegistrationAotPr
TypeContributor.contribute(type, it -> true, generationContext);
lazyLoadingProxyAotProcessor.registerLazyLoadingProxyIfNeeded(type, generationContext);
});
boolean enabled = Boolean.parseBoolean(
repositoryContext.getEnvironment().getProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "false"));
if (!enabled) {
return null;
}
return new MongoRepositoryContributor(repositoryContext);
}
@Override

2
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java

@ -65,7 +65,7 @@ import org.springframework.util.ObjectUtils; @@ -65,7 +65,7 @@ import org.springframework.util.ObjectUtils;
* @author Christoph Strobl
* @author Edward Prentice
*/
class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
public class MongoQueryCreator extends AbstractQueryCreator<Query, Criteria> {
private static final Log LOG = LogFactory.getLog(MongoQueryCreator.class);

59
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java

@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
*/
package org.springframework.data.mongodb.repository.query;
import java.util.Iterator;
import java.util.List;
import java.util.function.Supplier;
@ -32,6 +33,9 @@ import org.springframework.data.geo.Point; @@ -32,6 +33,9 @@ import org.springframework.data.geo.Point;
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;
@ -55,7 +59,7 @@ import com.mongodb.client.result.DeleteResult; @@ -55,7 +59,7 @@ import com.mongodb.client.result.DeleteResult;
* @author Christoph Strobl
*/
@FunctionalInterface
interface MongoQueryExecution {
public interface MongoQueryExecution {
@Nullable
Object execute(Query query);
@ -67,12 +71,12 @@ interface MongoQueryExecution { @@ -67,12 +71,12 @@ interface MongoQueryExecution {
* @author Christoph Strobl
* @since 1.5
*/
final class SlicedExecution implements MongoQueryExecution {
final class SlicedExecution<T> implements MongoQueryExecution {
private final FindWithQuery<?> find;
private final FindWithQuery<T> find;
private final Pageable pageable;
public SlicedExecution(ExecutableFindOperation.FindWithQuery<?> find, Pageable pageable) {
public SlicedExecution(ExecutableFindOperation.FindWithQuery<T> find, Pageable pageable) {
Assert.notNull(find, "Find must not be null");
Assert.notNull(pageable, "Pageable must not be null");
@ -83,7 +87,7 @@ interface MongoQueryExecution { @@ -83,7 +87,7 @@ interface MongoQueryExecution {
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public Object execute(Query query) {
public Slice<T> execute(Query query) {
int pageSize = pageable.getPageSize();
@ -93,7 +97,7 @@ interface MongoQueryExecution { @@ -93,7 +97,7 @@ interface MongoQueryExecution {
boolean hasNext = result.size() > pageSize;
return new SliceImpl<Object>(hasNext ? result.subList(0, pageSize) : result, pageable, hasNext);
return new SliceImpl<T>(hasNext ? result.subList(0, pageSize) : result, pageable, hasNext);
}
}
@ -104,12 +108,12 @@ interface MongoQueryExecution { @@ -104,12 +108,12 @@ interface MongoQueryExecution {
* @author Mark Paluch
* @author Christoph Strobl
*/
final class PagedExecution implements MongoQueryExecution {
final class PagedExecution<T> implements MongoQueryExecution {
private final FindWithQuery<?> operation;
private final FindWithQuery<T> operation;
private final Pageable pageable;
public PagedExecution(ExecutableFindOperation.FindWithQuery<?> operation, Pageable pageable) {
public PagedExecution(ExecutableFindOperation.FindWithQuery<T> operation, Pageable pageable) {
Assert.notNull(operation, "Operation must not be null");
Assert.notNull(pageable, "Pageable must not be null");
@ -119,11 +123,11 @@ interface MongoQueryExecution { @@ -119,11 +123,11 @@ interface MongoQueryExecution {
}
@Override
public Object execute(Query query) {
public Page<T> execute(Query query) {
int overallLimit = query.getLimit();
TerminatingFind<?> matching = operation.matching(query);
TerminatingFind<T> matching = operation.matching(query);
// Apply raw pagination
query.with(pageable);
@ -247,6 +251,39 @@ interface MongoQueryExecution { @@ -247,6 +251,39 @@ interface MongoQueryExecution {
}
}
final class DeleteExecutionX<T> implements MongoQueryExecution {
ExecutableRemoveOperation.ExecutableRemove<T> remove;
Type type;
public DeleteExecutionX(ExecutableRemove<T> remove, Type type) {
this.remove = remove;
this.type = type;
}
@Nullable
@Override
public Object execute(Query query) {
TerminatingRemove<T> 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<T> 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.
*

3
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CrudMethodMetadataPostProcessor.java

@ -39,6 +39,7 @@ import org.springframework.util.ClassUtils; @@ -39,6 +39,7 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import com.mongodb.ReadPreference;
import org.springframework.util.StringUtils;
/**
* {@link RepositoryProxyPostProcessor} that sets up interceptors to read metadata information from the invoked method.
@ -193,7 +194,7 @@ class CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor, B @@ -193,7 +194,7 @@ class CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor, B
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()));
}
}

67
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java

@ -29,11 +29,38 @@ import java.util.function.BiFunction; @@ -29,11 +29,38 @@ import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.StreamSupport;
import org.bson.*;
import org.bson.AbstractBsonWriter;
import org.bson.BSONObject;
import org.bson.BsonArray;
import org.bson.BsonBinary;
import org.bson.BsonBinarySubType;
import org.bson.BsonBoolean;
import org.bson.BsonContextType;
import org.bson.BsonDateTime;
import org.bson.BsonDbPointer;
import org.bson.BsonDecimal128;
import org.bson.BsonDouble;
import org.bson.BsonInt32;
import org.bson.BsonInt64;
import org.bson.BsonJavaScript;
import org.bson.BsonNull;
import org.bson.BsonObjectId;
import org.bson.BsonReader;
import org.bson.BsonRegularExpression;
import org.bson.BsonString;
import org.bson.BsonSymbol;
import org.bson.BsonTimestamp;
import org.bson.BsonUndefined;
import org.bson.BsonValue;
import org.bson.BsonWriter;
import org.bson.BsonWriterSettings;
import org.bson.Document;
import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.DocumentCodec;
import org.bson.codecs.EncoderContext;
import org.bson.codecs.configuration.CodecConfigurationException;
import org.bson.codecs.configuration.CodecRegistries;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.conversions.Bson;
import org.bson.json.JsonParseException;
@ -45,6 +72,8 @@ import org.springframework.core.convert.converter.Converter; @@ -45,6 +72,8 @@ import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mongodb.CodecRegistryProvider;
import org.springframework.data.mongodb.core.mapping.FieldName;
import org.springframework.data.mongodb.core.mapping.FieldName.Type;
import org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder;
import org.springframework.data.mongodb.util.json.SpringJsonWriter;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@ -73,6 +102,9 @@ public class BsonUtils { @@ -73,6 +102,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")
@Contract("null, _ -> null")
public static <T> @Nullable T get(@Nullable Bson bson, String key) {
@ -751,6 +783,17 @@ public class BsonUtils { @@ -751,6 +783,17 @@ public class BsonUtils {
return new Document(target);
}
public static JsonWriter writeJson(Document document) {
return sink -> {
SpringJsonWriter writer = new SpringJsonWriter(sink);
JSON_CODEC_REGISTRY.get(Document.class).encode(writer, document, EncoderContext.builder().build());
};
}
public interface JsonWriter {
void to(StringBuffer sink);
}
@Contract("null -> null")
private static @Nullable String toJson(@Nullable Object value) {
@ -963,4 +1006,26 @@ public class BsonUtils { @@ -963,4 +1006,26 @@ public class BsonUtils {
values.clear();
}
}
static class PlaceholderCodec implements Codec<Placeholder> {
@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<Placeholder> getEncoderClass() {
return Placeholder.class;
}
}
}

478
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/SpringJsonWriter.java

@ -0,0 +1,478 @@ @@ -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": "<number>" }
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": <t>, "i": <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;
}
}
}

102
spring-data-mongodb/src/test/java/example/aot/User.java

@ -0,0 +1,102 @@ @@ -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;
}
}

29
spring-data-mongodb/src/test/java/example/aot/UserProjection.java

@ -0,0 +1,29 @@ @@ -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();
}

146
spring-data-mongodb/src/test/java/example/aot/UserRepository.java

@ -0,0 +1,146 @@ @@ -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<User, String> {
/* Derived Queries */
List<User> findUserNoArgumentsBy();
User findOneByUsername(String username);
Optional<User> findOptionalOneByUsername(String username);
Long countUsersByLastname(String lastname);
Boolean existsUserByLastname(String lastname);
List<User> findByLastnameStartingWith(String lastname);
List<User> findTop2ByLastnameStartingWith(String lastname);
List<User> findByLastnameStartingWithOrderByUsername(String lastname);
List<User> findByLastnameStartingWith(String lastname, Limit limit);
List<User> findByLastnameStartingWith(String lastname, Sort sort);
List<User> findByLastnameStartingWith(String lastname, Sort sort, Limit limit);
List<User> findByLastnameStartingWith(String lastname, Pageable page);
Page<User> findPageOfUsersByLastnameStartingWith(String lastname, Pageable page);
Slice<User> 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<User> findAnnotatedQueryByLastname(String lastname);
@Query("""
{
'lastname' : {
'$regex' : '^?0'
}
}""")
List<User> findAnnotatedMultilineQueryByLastname(String username);
@Query("{ 'lastname' : { '$regex' : '^?0' } }")
List<User> findAnnotatedQueryByLastname(String lastname, Limit limit);
@Query("{ 'lastname' : { '$regex' : '^?0' } }")
List<User> findAnnotatedQueryByLastname(String lastname, Sort sort);
@Query("{ 'lastname' : { '$regex' : '^?0' } }")
List<User> findAnnotatedQueryByLastname(String lastname, Limit limit, Sort sort);
@Query("{ 'lastname' : { '$regex' : '^?0' } }")
List<User> findAnnotatedQueryByLastname(String lastname, Pageable pageable);
@Query("{ 'lastname' : { '$regex' : '^?0' } }")
Page<User> findAnnotatedQueryPageOfUsersByLastname(String lastname, Pageable pageable);
@Query("{ 'lastname' : { '$regex' : '^?0' } }")
Slice<User> 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<User> deleteUsersByLastnameStartingWith(String lastname);
@Query(value = "{ 'lastname' : { '$regex' : '^?0' } }", delete = true)
List<User> deleteUsersAnnotatedQueryByLastnameStartingWith(String lastname);
// TODO: updates
// TODO: Aggregations
/* Derived With Annotated Options */
@Query(sort = "{ 'username' : 1 }")
List<User> findWithAnnotatedSortByLastnameStartingWith(String lastname);
@Query(fields = "{ 'username' : 1 }")
List<User> findWithAnnotatedFieldsProjectionByLastnameStartingWith(String lastname);
@ReadPreference("no-such-read-preference")
User findWithReadPreferenceByUsername(String username);
// TODO: hints
/* Projecting Queries */
List<UserProjection> findUserProjectionByLastnameStartingWith(String lastname);
Page<UserProjection> findUserProjectionByLastnameStartingWith(String lastname, Pageable page);
}

62
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/DemoRepo.java

@ -0,0 +1,62 @@ @@ -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<User> 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();
}
}

662
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java

@ -0,0 +1,662 @@ @@ -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> 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<User> 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<User> users = methodInvoker.invoke("findByLastnameStartingWith", "S").onBean("aotUserRepository");
assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("luke", "vader", "kylo", "han");
});
}
@Test
void testLimitedDerivedFinder() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findTop2ByLastnameStartingWith", "S").onBean("aotUserRepository");
assertThat(users).hasSize(2);
});
}
@Test
void testSortedDerivedFinder() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findByLastnameStartingWithOrderByUsername", "S")
.onBean("aotUserRepository");
assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader");
});
}
@Test
void testDerivedFinderWithLimitArgument() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findByLastnameStartingWith", "S", Limit.of(2))
.onBean("aotUserRepository");
assertThat(users).hasSize(2);
});
}
@Test
void testDerivedFinderWithSort() {
generated.verify(methodInvoker -> {
List<User> 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<User> 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<User> 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<User> 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<User> 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<User> users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S").onBean("aotUserRepository");
assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader");
});
}
@Test
void testAnnotatedMultilineFinderWithQuery() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findAnnotatedMultilineQueryByLastname", "S").onBean("aotUserRepository");
assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader");
});
}
@Test
void testAnnotatedFinderWithQueryAndLimit() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Limit.of(2))
.onBean("aotUserRepository");
assertThat(users).hasSize(2);
});
}
@Test
void testAnnotatedFinderWithQueryAndSort() {
generated.verify(methodInvoker -> {
List<User> 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<User> 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<User> 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<User> 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<User> 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<User> 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<User> users = methodInvoker.invoke("findWithAnnotatedSortByLastnameStartingWith", "S")
.onBean("aotUserRepository");
assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader");
});
}
@Test
void testDerivedFinderWithAnnotatedFieldsProjection() {
generated.verify(methodInvoker -> {
List<User> 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<UserProjection> users = methodInvoker.invoke("findUserProjectionByLastnameStartingWith", "S")
.onBean("aotUserRepository");
assertThat(users).extracting(UserProjection::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke",
"vader");
});
}
@Test
void testDerivedFinderReturningPageOfProjections() {
generated.verify(methodInvoker -> {
Page<UserProjection> 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<String, BeanDefinition> beanDefinitions = new LinkedHashMap<>();
Lazy<DefaultListableBeanFactory> 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<String, BeanDefinition> 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<GeneratedContext> methodInvoker) {
methodInvoker.accept(new GeneratedContext(lazyFactory));
return this;
}
}
interface Verifyer {
Verifyer verify(Consumer<GeneratedContext> methodInvoker);
}
static class GeneratedContext {
private Supplier<DefaultListableBeanFactory> delegate;
public GeneratedContext(Supplier<DefaultListableBeanFactory> defaultListableBeanFactory) {
this.delegate = defaultListableBeanFactory;
}
InvocationBuilder invoke(String method, Object... arguments) {
return new InvocationBuilder() {
@Override
public <T> T onBean(String beanName) {
Object bean = delegate.get().getBean(beanName);
return ReflectionTestUtils.invokeMethod(bean, method, arguments);
}
};
}
interface InvocationBuilder {
<T> T onBean(String beanName);
}
}
}

144
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/StubRepositoryInformation.java

@ -0,0 +1,144 @@ @@ -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<Class<?>> getAlternativeDomainTypes() {
return null;
}
@Override
public boolean isReactiveRepository() {
return false;
}
@Override
public Set<RepositoryFragment<?>> 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<Method> getQueryMethods() {
return null;
}
@Override
public Class<?> getRepositoryBaseClass() {
return SimpleMongoRepository.class;
}
@Override
public Method getTargetClassMethod(Method method) {
return null;
}
}

129
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/TestMongoAotRepositoryContext.java

@ -0,0 +1,129 @@ @@ -0,0 +1,129 @@
/*
* 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.env.Environment;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.test.tools.ClassFile;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.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;
private final Environment environment = new StandardEnvironment();
public TestMongoAotRepositoryContext(Class<?> repositoryInterface, @Nullable RepositoryComposition composition) {
this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition);
}
@Override
public ConfigurableListableBeanFactory getBeanFactory() {
return null;
}
@Override
public TypeIntrospector introspectType(String typeName) {
return null;
}
@Override
public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) {
return null;
}
@Override
public String getBeanName() {
return "dummyRepository";
}
@Override
public Set<String> getBasePackages() {
return Set.of("org.springframework.data.dummy.repository.aot");
}
@Override
public Set<Class<? extends Annotation>> getIdentifyingAnnotations() {
return Set.of(Document.class);
}
@Override
public RepositoryInformation getRepositoryInformation() {
return repositoryInformation;
}
@Override
public Set<MergedAnnotation<Annotation>> getResolvedAnnotations() {
return Set.of();
}
@Override
public Set<Class<?>> getResolvedTypes() {
return Set.of();
}
public List<ClassFile> getRequiredContextFiles() {
return List.of(classFileForType(repositoryInformation.getRepositoryBaseClass()));
}
static ClassFile classFileForType(Class<?> type) {
String name = type.getName();
ClassPathResource cpr = new ClassPathResource(name.replaceAll("\\.", "/") + ".class");
try {
return ClassFile.of(name, cpr.getContentAsByteArray());
} catch (IOException e) {
throw new IllegalArgumentException("Cannot open [%s].".formatted(cpr.getPath()));
}
}
@Override
public Environment getEnvironment() {
return environment;
}
}

9
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java

@ -23,6 +23,7 @@ import java.util.function.Consumer; @@ -23,6 +23,7 @@ import java.util.function.Consumer;
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;
@ -44,6 +45,14 @@ public class MongoTestTemplate extends MongoTemplate { @@ -44,6 +45,14 @@ public class MongoTestTemplate extends MongoTemplate {
private final MongoTestTemplateConfiguration cfg;
public MongoTestTemplate() {
this("test");
}
public MongoTestTemplate(String databaseName) {
this(MongoClients.create(), databaseName);
}
public MongoTestTemplate(MongoClient client, String database, Class<?>... initialEntities) {
this(cfg -> {
cfg.configureDatabaseFactory(it -> {

159
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/SpringJsonWriterUnitTests.java

@ -0,0 +1,159 @@ @@ -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'}]");
}
}

1
spring-data-mongodb/src/test/resources/logback.xml

@ -18,6 +18,7 @@ @@ -18,6 +18,7 @@
<appender-ref ref="no-op" />
</logger>
<logger name="org.springframework.data.mongodb.test.util" level="info"/>
<logger name="org.springframework.data.repository.aot.generate.RepositoryContributor" level="trace" />
<root level="error">
<appender-ref ref="console" />

Loading…
Cancel
Save