io.micronaut.data.mongodb.operations.DefaultMongoStoredQuery Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of micronaut-data-mongodb Show documentation
Show all versions of micronaut-data-mongodb Show documentation
Data Repository Support for Micronaut
/*
* Copyright 2017-2022 original 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 io.micronaut.data.mongodb.operations;
import com.mongodb.client.model.Collation;
import com.mongodb.client.model.DeleteOptions;
import com.mongodb.client.model.UpdateOptions;
import io.micronaut.aop.InvocationContext;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.convert.ConversionContext;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.StringUtils;
import io.micronaut.data.annotation.Query;
import io.micronaut.data.document.model.query.builder.MongoQueryBuilder;
import io.micronaut.data.exceptions.DataAccessException;
import io.micronaut.data.intercept.annotation.DataMethod;
import io.micronaut.data.model.PersistentPropertyPath;
import io.micronaut.data.model.runtime.AttributeConverterRegistry;
import io.micronaut.data.model.runtime.QueryParameterBinding;
import io.micronaut.data.model.runtime.RuntimeAssociation;
import io.micronaut.data.model.runtime.RuntimeEntityRegistry;
import io.micronaut.data.model.runtime.RuntimePersistentEntity;
import io.micronaut.data.model.runtime.RuntimePersistentProperty;
import io.micronaut.data.model.runtime.StoredQuery;
import io.micronaut.data.model.runtime.convert.AttributeConverter;
import io.micronaut.data.mongodb.annotation.MongoCollation;
import io.micronaut.data.mongodb.annotation.MongoProjection;
import io.micronaut.data.mongodb.annotation.MongoSort;
import io.micronaut.data.mongodb.operations.options.MongoAggregationOptions;
import io.micronaut.data.mongodb.operations.options.MongoFindOptions;
import io.micronaut.data.mongodb.operations.options.MongoOptionsUtils;
import io.micronaut.data.runtime.operations.internal.query.DefaultBindableParametersStoredQuery;
import io.micronaut.data.runtime.query.internal.DefaultStoredQuery;
import io.micronaut.data.runtime.query.internal.DelegateStoredQuery;
import org.bson.BsonArray;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonObjectId;
import org.bson.BsonRegularExpression;
import org.bson.BsonValue;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Default implementation of {@link MongoStoredQuery}.
*
* @param The entity type
* @param The result type
* @author Denis Stepanov
* @since 3.3.
*/
@Internal
final class DefaultMongoStoredQuery extends DefaultBindableParametersStoredQuery implements DelegateStoredQuery, MongoStoredQuery {
private static final Pattern MONGO_PARAM_PATTERN = Pattern.compile("\\W*(\\" + MongoQueryBuilder.QUERY_PARAMETER_PLACEHOLDER + ":(\\d)+)\\W*");
private static final Logger LOG = LoggerFactory.getLogger(DefaultMongoStoredQuery.class);
private static final BsonDocument EMPTY = new BsonDocument();
private final StoredQuery storedQuery;
private final Supplier codecRegistry;
private final AttributeConverterRegistry attributeConverterRegistry;
private final RuntimeEntityRegistry runtimeEntityRegistry;
private final ConversionService conversionService;
private final RuntimePersistentEntity persistentEntity;
private final UpdateData updateData;
private final FindData findData;
private final AggregateData aggregateData;
private final DeleteData deleteData;
private final boolean isCount;
DefaultMongoStoredQuery(StoredQuery storedQuery,
Supplier codecRegistry,
AttributeConverterRegistry attributeConverterRegistry,
RuntimeEntityRegistry runtimeEntityRegistry,
ConversionService conversionService,
RuntimePersistentEntity persistentEntity) {
this(storedQuery,
codecRegistry,
attributeConverterRegistry,
runtimeEntityRegistry,
conversionService,
persistentEntity,
storedQuery.getAnnotationMetadata().stringValue(Query.class, "update").orElse(null));
}
DefaultMongoStoredQuery(StoredQuery storedQuery,
Supplier codecRegistry,
AttributeConverterRegistry attributeConverterRegistry,
RuntimeEntityRegistry runtimeEntityRegistry,
ConversionService conversionService,
RuntimePersistentEntity persistentEntity,
String updateJson) {
super(storedQuery, persistentEntity);
this.storedQuery = storedQuery;
this.codecRegistry = codecRegistry;
this.attributeConverterRegistry = attributeConverterRegistry;
this.runtimeEntityRegistry = runtimeEntityRegistry;
this.conversionService = conversionService;
this.persistentEntity = persistentEntity;
OperationType operationType = storedQuery.getOperationType();
if (operationType == OperationType.QUERY || operationType == OperationType.EXISTS || operationType == OperationType.COUNT) {
String query = storedQuery.getQuery();
String filterParameter = getParameterInRole(MongoRoles.FILTER_ROLE);
String filterOptionsParameter = getParameterInRole(MongoRoles.FIND_OPTIONS_ROLE);
String pipelineParameter = getParameterInRole(MongoRoles.PIPELINE_ROLE);
if (filterParameter != null || filterOptionsParameter != null) {
aggregateData = null;
findData = new FindData(filterParameter, filterOptionsParameter);
} else if (pipelineParameter != null) {
aggregateData = new AggregateData(pipelineParameter, getParameterInRole(MongoRoles.AGGREGATE_OPTIONS_ROLE));
findData = null;
} else if (StringUtils.isEmpty(query)) {
aggregateData = null;
findData = new FindData(BsonDocument.parse(query));
} else if (query.startsWith("[")) {
aggregateData = new AggregateData(parseAggregation(query, storedQuery.isCount()));
findData = null;
} else {
aggregateData = null;
findData = new FindData(BsonDocument.parse(query));
}
isCount = operationType == OperationType.COUNT || storedQuery.isCount() || query.contains("$count");
} else {
aggregateData = null;
findData = null;
isCount = false;
}
if (operationType == OperationType.DELETE) {
String query = storedQuery.getQuery();
deleteData = new DeleteData(
StringUtils.isEmpty(query) ? EMPTY : BsonDocument.parse(query),
getParameterInRole(MongoRoles.FILTER_ROLE),
getParameterInRole(MongoRoles.DELETE_OPTIONS_ROLE)
);
} else {
deleteData = null;
}
if (operationType == OperationType.UPDATE) {
if (StringUtils.isEmpty(updateJson)) {
throw new IllegalStateException("Update query is expected!");
}
String query = storedQuery.getQuery();
updateData = new UpdateData(
BsonDocument.parse(updateJson), StringUtils.isEmpty(query) ? EMPTY : BsonDocument.parse(query),
getParameterInRole(MongoRoles.FILTER_ROLE),
getParameterInRole(MongoRoles.UPDATE_ROLE),
getParameterInRole(MongoRoles.UPDATE_OPTIONS_ROLE)
);
} else {
updateData = null;
}
}
private List parseAggregation(String query, boolean isCount) {
List pipeline = BsonArray.parse(query).stream().map(BsonValue::asDocument).toList();
if (isCount && pipeline.stream().noneMatch(p -> p.toBsonDocument().containsKey("$count"))) {
// We can probably remove sorting projection etc. or allow a user to specify a custom count pipeline
List countPipeline = new ArrayList<>(pipeline);
countPipeline.add(BsonDocument.parse("{ $count: \"totalCount\" }"));
return countPipeline;
}
return pipeline;
}
@Override
public boolean isCount() {
return isCount;
}
@Nullable
private String getParameterInRole(String role) {
if (storedQuery instanceof DefaultStoredQuery) {
return storedQuery.getAnnotationMetadata().getAnnotation(DataMethod.class).stringValue(role).orElse(null);
}
return null;
}
@Nullable
private int getParameterIndexByName(@Nullable String name) {
if (name == null) {
return -1;
}
if (storedQuery instanceof DefaultStoredQuery, ?> defaultStoredQuery) {
String[] argumentNames = defaultStoredQuery.getMethod().getArgumentNames();
for (int i = 0; i < argumentNames.length; i++) {
String argumentName = argumentNames[i];
if (argumentName.equals(name)) {
return i;
}
}
throw new IllegalStateException("Unknown parameter with name: " + name);
}
throw new IllegalStateException("Expected DefaultStoredQuery");
}
@Nullable
private X getParameterAtIndex(InvocationContext, ?> invocationContext, int index) {
requireInvocationContext(invocationContext);
return (X) invocationContext.getParameterValues()[index];
}
@Override
public RuntimePersistentEntity getRuntimePersistentEntity() {
return persistentEntity;
}
@Override
public boolean isAggregate() {
return aggregateData != null;
}
@Override
public MongoAggregation getAggregation(InvocationContext, ?> invocationContext) {
if (aggregateData == null) {
throw new IllegalStateException("Expected aggregation query!");
}
return aggregateData.getAggregation(invocationContext);
}
@Override
public MongoFind getFind(InvocationContext, ?> invocationContext) {
if (findData == null) {
throw new IllegalStateException("Expected find query!");
}
return findData.getFind(invocationContext);
}
@Override
public MongoUpdate getUpdateMany(InvocationContext, ?> invocationContext) {
if (updateData == null) {
throw new IllegalStateException("Expected update query!");
}
return updateData.getUpdateMany(invocationContext);
}
@Override
public MongoUpdate getUpdateOne(E entity) {
if (updateData == null) {
throw new IllegalStateException("Expected update query!");
}
return updateData.getUpdateOne(entity);
}
@Override
public MongoDelete getDeleteMany(InvocationContext, ?> invocationContext) {
if (deleteData == null) {
throw new IllegalStateException("Expected delete query!");
}
return deleteData.getDeleteMany(invocationContext);
}
@Override
public MongoDelete getDeleteOne(E entity) {
if (deleteData == null) {
throw new IllegalStateException("Expected delete query!");
}
return deleteData.getDeleteOne(entity);
}
private boolean needsProcessing(Bson value) {
if (value == null) {
return false;
}
if (value instanceof BsonDocument) {
return needsProcessingValue(value.toBsonDocument());
}
throw new IllegalStateException("Unrecognized value: " + value);
}
private boolean needsProcessing(List values) {
if (values == null) {
return false;
}
for (Bson value : values) {
if (needsProcessing(value)) {
return true;
}
}
return false;
}
private boolean needsProcessingValue(BsonValue value) {
if (value instanceof BsonDocument bsonDocument) {
BsonInt32 queryParameterIndex = bsonDocument.getInt32(MongoQueryBuilder.QUERY_PARAMETER_PLACEHOLDER, null);
if (queryParameterIndex != null) {
return true;
}
for (Map.Entry entry : bsonDocument.entrySet()) {
BsonValue bsonValue = entry.getValue();
if (needsProcessingValue(bsonValue)) {
return true;
}
}
return false;
}
if (value instanceof BsonArray bsonArray) {
for (BsonValue bsonValue : bsonArray) {
if (needsProcessingValue(bsonValue)) {
return true;
}
}
}
if (value instanceof BsonRegularExpression bsonRegularExpression) {
String pattern = bsonRegularExpression.getPattern();
return MONGO_PARAM_PATTERN.matcher(pattern).matches();
}
return false;
}
private Bson replaceQueryParameters(Bson value, @Nullable InvocationContext, ?> invocationContext, @Nullable E entity) {
if (value instanceof BsonDocument bsonDocument) {
return (BsonDocument) replaceQueryParametersInBsonValue(bsonDocument.clone(), invocationContext, entity);
}
throw new IllegalStateException("Unrecognized value: " + value);
}
private List replaceQueryParametersInList(List values, @Nullable InvocationContext, ?> invocationContext, @Nullable E entity) {
values = new ArrayList<>(values);
for (int i = 0; i < values.size(); i++) {
Bson value = values.get(i);
Bson newValue = replaceQueryParameters(value, invocationContext, entity);
if (value != newValue) {
values.set(i, newValue);
}
}
return values;
}
private Map.Entry bind(QueryParameterBinding queryParameterBinding, @Nullable InvocationContext, ?> invocationContext, @Nullable E entity) {
Object[] holder = new Object[1];
bindParameter(new Binder() {
@Override
public Object autoPopulateRuntimeProperty(RuntimePersistentProperty> persistentProperty, Object previousValue) {
return runtimeEntityRegistry.autoPopulateRuntimeProperty(persistentProperty, previousValue);
}
@Override
public Object convert(Object value, RuntimePersistentProperty> property) {
AttributeConverter
© 2015 - 2025 Weber Informatics LLC | Privacy Policy