Browse Source
Initial Support for generating repository source code at build time. Closes: #4939pull/4976/head
23 changed files with 2922 additions and 16 deletions
@ -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(); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -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(); |
||||
} |
||||
|
||||
} |
||||
} |
||||
@ -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())); |
||||
} |
||||
} |
||||
@ -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 |
||||
} |
||||
} |
||||
@ -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; |
||||
} |
||||
} |
||||
} |
||||
@ -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; |
||||
} |
||||
} |
||||
@ -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(); |
||||
} |
||||
@ -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); |
||||
|
||||
} |
||||
@ -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(); |
||||
} |
||||
} |
||||
@ -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); |
||||
} |
||||
|
||||
} |
||||
} |
||||
@ -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; |
||||
} |
||||
} |
||||
@ -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; |
||||
} |
||||
} |
||||
@ -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'}]"); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue