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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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