Criteria.java
/*
* Copyright 2010-present 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.core.query;
import static org.springframework.util.ObjectUtils.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.bson.BsonRegularExpression;
import org.bson.BsonType;
import org.bson.Document;
import org.bson.types.Binary;
import org.jspecify.annotations.Nullable;
import org.springframework.data.core.TypedPropertyPath;
import org.springframework.data.domain.Example;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Point;
import org.springframework.data.geo.Shape;
import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
import org.springframework.data.mongodb.MongoExpression;
import org.springframework.data.mongodb.core.geo.GeoJson;
import org.springframework.data.mongodb.core.geo.Sphere;
import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type;
import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.data.mongodb.util.RegexFlags;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import com.mongodb.BasicDBList;
/**
* Central class for creating queries. It follows a fluent API style so that you can easily chain together multiple
* criteria. Static import of the {@link Criteria#where Criteria.where} method improves readability.
*
* @author Thomas Risberg
* @author Oliver Gierke
* @author Thomas Darimont
* @author Christoph Strobl
* @author Mark Paluch
* @author Andreas Zink
* @author Ziemowit Stolarczyk
* @author Cl��ment Petit
* @author James McNee
*/
public class Criteria implements CriteriaDefinition {
/**
* Custom "not-null" object as we have to be able to work with {@literal null} values as well.
*/
private static final Object NOT_SET = new Object();
private @Nullable String key;
private final List<Criteria> criteriaChain;
private final LinkedHashMap<String, @Nullable Object> criteria = new LinkedHashMap<String, Object>();
private @Nullable Object isValue = NOT_SET;
public Criteria() {
this.criteriaChain = new ArrayList<>();
}
public Criteria(String key) {
this.criteriaChain = new ArrayList<>();
this.criteriaChain.add(this);
this.key = key;
}
protected Criteria(List<Criteria> criteriaChain, String key) {
this.criteriaChain = new ArrayList<>(criteriaChain);
this.criteriaChain.add(this);
this.key = key;
}
/**
* Static factory method to create a Criteria using the provided key
*
* @param key the property or field name.
* @return new instance of {@link Criteria}.
*/
public static Criteria where(String key) {
return new Criteria(key);
}
/**
* Static factory method to create a Criteria using the provided path.
*
* @param property the path to the target property.
* @return new instance of {@link Criteria}.
* @since 5.1
*/
public static <T, P> Criteria where(TypedPropertyPath<T, P> property) {
return where(property.toDotPath());
}
/**
* Static factory method to create a {@link Criteria} matching an example object.
*
* @param example must not be {@literal null}.
* @return new instance of {@link Criteria}.
* @see Criteria#alike(Example)
* @since 1.8
*/
public static Criteria byExample(Object example) {
return byExample(Example.of(example));
}
/**
* Static factory method to create a {@link Criteria} matching an example object. <br />
* By default, the {@link Example} uses typed matching restricting it to probe assignable types. For example, when
* sticking with the default type key ({@code _class}), the query has restrictions such as
* <code>_class : { $in : [com.acme.Person] } </code>. <br />
* To avoid the above-mentioned type restriction use an {@link UntypedExampleMatcher} with
* {@link Example#of(Object, org.springframework.data.domain.ExampleMatcher)}.
*
* @param example must not be {@literal null}.
* @return new instance of {@link Criteria}.
* @see Criteria#alike(Example)
* @since 1.8
*/
public static Criteria byExample(Example<?> example) {
return new Criteria().alike(example);
}
/**
* Static factory method to create a {@link Criteria} matching documents against a given structure defined by the
* {@link MongoJsonSchema} using ({@code $jsonSchema}) operator.
*
* @param schema must not be {@literal null}.
* @return this
* @since 2.1
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/">MongoDB Query operator:
* $jsonSchema</a>
*/
public static Criteria matchingDocumentStructure(MongoJsonSchema schema) {
return new Criteria().andDocumentStructureMatches(schema);
}
/**
* Static factory method to create a {@link Criteria} matching a documents against the given {@link MongoExpression
* expression}.
* <p>
* The {@link MongoExpression expression} can be either something that directly renders to the store native
* representation like
*
* <pre class="code">
* expr(() -> Document.parse("{ $gt : [ '$spent', '$budget'] }")))
* </pre>
*
* or an {@link org.springframework.data.mongodb.core.aggregation.AggregationExpression} which will be subject to
* context (domain type) specific field mapping.
*
* <pre class="code">
* expr(valueOf("amountSpent").greaterThan("budget"))
* </pre>
*
* @param expression must not be {@literal null}.
* @return new instance of {@link Criteria}.
* @since 4.1
*/
public static Criteria expr(MongoExpression expression) {
Assert.notNull(expression, "Expression must not be null");
Criteria criteria = new Criteria();
criteria.criteria.put("$expr", expression);
return criteria;
}
/**
* Static factory method to create a Criteria using the provided key
*
* @return new instance of {@link Criteria}.
*/
@Contract("_ -> new")
public Criteria and(String key) {
return new Criteria(this.criteriaChain, key);
}
/**
* Static factory method to create a Criteria using the provided path.
*
* @param property path to the target property.
* @return new instance of {@link Criteria}.
* @since 5.1
*/
@Contract("_ -> new")
public <T, P> Criteria and(TypedPropertyPath<T, P> property) {
return and(TypedPropertyPath.of(property).toDotPath());
}
/**
* Creates a criterion using equality
*
* @param value can be {@literal null}.
* @return this.
*/
@Contract("_ -> this")
public Criteria is(@Nullable Object value) {
if (lastOperatorWasNot()) {
return eq(value);
}
if (!NOT_SET.equals(isValue)) {
throw new InvalidMongoDbApiUsageException(
"Multiple 'is' values declared; You need to use 'and' with multiple criteria");
}
this.isValue = value;
return this;
}
/**
* Creates a criterion using {@literal null} equality comparison which matches documents that either contain the item
* field whose value is {@literal null} or that do not contain the item field. <br />
* Use {@link #isNullValue()} to only query for documents that contain the field whose value is equal to
* {@link org.bson.BsonType#NULL}. <br />
* Use {@link #exists(boolean)} to query for documents that do (not) contain the field.
*
* @return this.
* @see <a href="https://docs.mongodb.com/manual/tutorial/query-for-null-fields/#equality-filter">Query for Null or
* Missing Fields: Equality Filter</a>
* @since 3.3
*/
@Contract("-> this")
public Criteria isNull() {
return is(null);
}
/**
* Creates a criterion using a {@link org.bson.BsonType} comparison which matches only documents that contain the item
* field whose value is equal to {@link org.bson.BsonType#NULL}. <br />
* Use {@link #isNull()} to query for documents that contain the field with a {@literal null} value or do not contain
* the field at all. <br />
* Use {@link #exists(boolean)} to query for documents that do (not) contain the field.
*
* @return this.
* @see <a href="https://docs.mongodb.com/manual/tutorial/query-for-null-fields/#type-check">Query for Null or Missing
* Fields: Type Check</a>
* @since 3.3
*/
@Contract("-> this")
public Criteria isNullValue() {
criteria.put("$type", BsonType.NULL.getValue());
return this;
}
private boolean lastOperatorWasNot() {
return !this.criteria.isEmpty() && "$not".equals(this.criteria.keySet().toArray()[this.criteria.size() - 1]);
}
/**
* Creates a criterion using the {@literal $eq} operator.
*
* @param value can be {@literal null}.
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/eq/">MongoDB Query operator: $eq</a>
* @since 5.0.4
*/
@Contract("_ -> this")
public Criteria eq(@Nullable Object value) {
criteria.put("$eq", value);
return this;
}
/**
* Creates a criterion using the {@literal $ne} operator.
*
* @param value can be {@literal null}.
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/ne/">MongoDB Query operator: $ne</a>
*/
@Contract("_ -> this")
public Criteria ne(@Nullable Object value) {
criteria.put("$ne", value);
return this;
}
/**
* Creates a criterion using the {@literal $lt} operator.
*
* @param value must not be {@literal null}.
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/lt/">MongoDB Query operator: $lt</a>
*/
@Contract("_ -> this")
public Criteria lt(Object value) {
criteria.put("$lt", value);
return this;
}
/**
* Creates a criterion using the {@literal $lte} operator.
*
* @param value must not be {@literal null}.
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/lte/">MongoDB Query operator: $lte</a>
*/
@Contract("_ -> this")
public Criteria lte(Object value) {
criteria.put("$lte", value);
return this;
}
/**
* Creates a criterion using the {@literal $gt} operator.
*
* @param value must not be {@literal null}.
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/gt/">MongoDB Query operator: $gt</a>
*/
@Contract("_ -> this")
public Criteria gt(Object value) {
criteria.put("$gt", value);
return this;
}
/**
* Creates a criterion using the {@literal $gte} operator.
*
* @param value can be {@literal null}.
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/gte/">MongoDB Query operator: $gte</a>
*/
@Contract("_ -> this")
public Criteria gte(Object value) {
criteria.put("$gte", value);
return this;
}
/**
* Creates a criterion using the {@literal $in} operator.
*
* @param values the values to match against
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/in/">MongoDB Query operator: $in</a>
*/
@Contract("_ -> this")
public Criteria in(@Nullable Object ... values) {
if (values.length > 1 && values[1] instanceof Collection) {
throw new InvalidMongoDbApiUsageException(
"You can only pass in one argument of type " + values[1].getClass().getName());
}
return this.in(Arrays.asList(values));
}
/**
* Creates a criterion using the {@literal $in} operator.
*
* @param values the collection containing the values to match against
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/in/">MongoDB Query operator: $in</a>
*/
@Contract("_ -> this")
public Criteria in(Collection<?> values) {
criteria.put("$in", values);
return this;
}
/**
* Creates a criterion using the {@literal $nin} operator.
*
* @param values
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/nin/">MongoDB Query operator: $nin</a>
*/
@Contract("_ -> this")
public Criteria nin(Object... values) {
return nin(Arrays.asList(values));
}
/**
* Creates a criterion using the {@literal $nin} operator.
*
* @param values must not be {@literal null}.
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/nin/">MongoDB Query operator: $nin</a>
*/
@Contract("_ -> this")
public Criteria nin(Collection<?> values) {
criteria.put("$nin", values);
return this;
}
/**
* Creates a criterion using the {@literal $mod} operator.
*
* @param value must not be {@literal null}.
* @param remainder must not be {@literal null}.
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/mod/">MongoDB Query operator: $mod</a>
*/
@Contract("_, _ -> this")
public Criteria mod(Number value, Number remainder) {
List<Object> l = new ArrayList<>(2);
l.add(value);
l.add(remainder);
criteria.put("$mod", l);
return this;
}
/**
* Creates a criterion using the {@literal $all} operator.
*
* @param values must not be {@literal null}.
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/all/">MongoDB Query operator: $all</a>
*/
@Contract("_ -> this")
public Criteria all(Object... values) {
return all(Arrays.asList(values));
}
/**
* Creates a criterion using the {@literal $all} operator.
*
* @param values must not be {@literal null}.
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/all/">MongoDB Query operator: $all</a>
*/
@Contract("_ -> this")
public Criteria all(Collection<?> values) {
criteria.put("$all", values);
return this;
}
/**
* Creates a criterion using the {@literal $size} operator.
*
* @param size
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/size/">MongoDB Query operator: $size</a>
*/
@Contract("_ -> this")
public Criteria size(int size) {
criteria.put("$size", size);
return this;
}
/**
* Creates a criterion using the {@literal $exists} operator.
*
* @param value
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/exists/">MongoDB Query operator: $exists</a>
*/
@Contract("_ -> this")
public Criteria exists(boolean value) {
criteria.put("$exists", value);
return this;
}
/**
* Creates a criterion using the {@literal $sampleRate} operator.
*
* @param sampleRate sample rate to determine number of documents to be randomly selected from the input. Must be
* between {@code 0} and {@code 1}.
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/sampleRate/">MongoDB Query operator:
* $sampleRate</a>
* @since 3.3
*/
@Contract("_ -> this")
public Criteria sampleRate(double sampleRate) {
Assert.isTrue(sampleRate >= 0, "The sample rate must be greater than zero");
Assert.isTrue(sampleRate <= 1, "The sample rate must not be greater than one");
criteria.put("$sampleRate", sampleRate);
return this;
}
/**
* Creates a criterion using the {@literal $type} operator.
*
* @param typeNumber
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/type/">MongoDB Query operator: $type</a>
*/
@Contract("_ -> this")
public Criteria type(int typeNumber) {
criteria.put("$type", typeNumber);
return this;
}
/**
* Creates a criterion using the {@literal $type} operator.
*
* @param types must not be {@literal null}.
* @return this.
* @since 2.1
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/type/">MongoDB Query operator: $type</a>
*/
@Contract("_ -> this")
public Criteria type(Type... types) {
Assert.notNull(types, "Types must not be null");
Assert.noNullElements(types, "Types must not contain null");
return type(Arrays.asList(types));
}
/**
* Creates a criterion using the {@literal $type} operator.
*
* @param types must not be {@literal null}.
* @return this.
* @since 3.2
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/type/">MongoDB Query operator: $type</a>
*/
@Contract("_ -> this")
public Criteria type(Collection<Type> types) {
Assert.notNull(types, "Types must not be null");
criteria.put("$type", types.stream().map(Type::toBsonType).map(Type::value).collect(Collectors.toList()));
return this;
}
/**
* Creates a criterion using the {@literal $not} meta operator which affects the clause directly following
*
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/not/">MongoDB Query operator: $not</a>
*/
@Contract("-> this")
public Criteria not() {
return not(null);
}
/**
* Creates a criterion using the {@literal $not} operator.
*
* @param value can be {@literal null}.
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/not/">MongoDB Query operator: $not</a>
*/
@Contract("_ -> this")
private Criteria not(@Nullable Object value) {
criteria.put("$not", value);
return this;
}
/**
* Creates a criterion using a {@literal $regex} operator.
*
* @param regex must not be {@literal null}.
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/regex/">MongoDB Query operator: $regex</a>
*/
@Contract("_ -> this")
public Criteria regex(String regex) {
return regex(regex, null);
}
/**
* Creates a criterion using a {@literal $regex} and {@literal $options} operator.
*
* @param regex must not be {@literal null}.
* @param options can be {@literal null}.
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/regex/">MongoDB Query operator: $regex</a>
*/
@Contract("_, _ -> this")
public Criteria regex(String regex, @Nullable String options) {
return regex(toPattern(regex, options));
}
/**
* Syntactical sugar for {@link #is(Object)} making obvious that we create a regex predicate.
*
* @param pattern must not be {@literal null}.
* @return this.
*/
@Contract("_ -> this")
public Criteria regex(Pattern pattern) {
Assert.notNull(pattern, "Pattern must not be null");
if (lastOperatorWasNot()) {
return not(pattern);
}
this.isValue = pattern;
return this;
}
/**
* Use a MongoDB native {@link BsonRegularExpression}.
*
* @param regex must not be {@literal null}.
* @return this.
*/
@Contract("_ -> this")
public Criteria regex(BsonRegularExpression regex) {
if (lastOperatorWasNot()) {
return not(regex);
}
this.isValue = regex;
return this;
}
private Pattern toPattern(String regex, @Nullable String options) {
Assert.notNull(regex, "Regex string must not be null");
return Pattern.compile(regex, RegexFlags.toRegexFlags(options));
}
/**
* Creates a geospatial criterion using a {@literal $geoWithin $centerSphere} operation. This is only available for
* Mongo 2.4 and higher.
*
* @param circle must not be {@literal null}
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/geoWithin/">MongoDB Query operator:
* $geoWithin</a>
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/centerSphere/">MongoDB Query operator:
* $centerSphere</a>
*/
@Contract("_ -> this")
public Criteria withinSphere(Circle circle) {
Assert.notNull(circle, "Circle must not be null");
criteria.put("$geoWithin", new GeoCommand(new Sphere(circle)));
return this;
}
/**
* Creates a geospatial criterion using a {@literal $geoWithin} operation.
*
* @param shape must not be {@literal null}.
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/geoWithin/">MongoDB Query operator:
* $geoWithin</a>
*/
@Contract("_ -> this")
public Criteria within(Shape shape) {
Assert.notNull(shape, "Shape must not be null");
criteria.put("$geoWithin", new GeoCommand(shape));
return this;
}
/**
* Creates a geospatial criterion using a {@literal $near} operation.
*
* @param point must not be {@literal null}
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/near/">MongoDB Query operator: $near</a>
*/
@Contract("_ -> this")
public Criteria near(Point point) {
Assert.notNull(point, "Point must not be null");
criteria.put("$near", point);
return this;
}
/**
* Creates a geospatial criterion using a {@literal $nearSphere} operation. This is only available for Mongo 1.7 and
* higher.
*
* @param point must not be {@literal null}
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/nearSphere/">MongoDB Query operator:
* $nearSphere</a>
*/
@Contract("_ -> this")
public Criteria nearSphere(Point point) {
Assert.notNull(point, "Point must not be null");
criteria.put("$nearSphere", point);
return this;
}
/**
* Creates criterion using {@code $geoIntersects} operator which matches intersections of the given {@code geoJson}
* structure and the documents one. Requires MongoDB 2.4 or better.
*
* @param geoJson must not be {@literal null}.
* @return this.
* @since 1.8
*/
@SuppressWarnings("rawtypes")
@Contract("_ -> this")
public Criteria intersects(GeoJson geoJson) {
Assert.notNull(geoJson, "GeoJson must not be null");
criteria.put("$geoIntersects", geoJson);
return this;
}
/**
* Creates a geo-spatial criterion using a {@literal $maxDistance} operation, for use with {@literal $near} or
* {@literal $nearSphere}.
* <p>
* <strong>NOTE:</strong> The unit of measure for distance may depends on the used coordinate representation (legacy
* vs. geoJson) as well as the target operation.
*
* @param maxDistance radians or meters
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/maxDistance/">MongoDB Query operator:
* $maxDistance</a>
*/
@Contract("_ -> this")
public Criteria maxDistance(double maxDistance) {
if (createNearCriteriaForCommand("$near", "$maxDistance", maxDistance)
|| createNearCriteriaForCommand("$nearSphere", "$maxDistance", maxDistance)) {
return this;
}
criteria.put("$maxDistance", maxDistance);
return this;
}
/**
* Creates a geospatial criterion using a {@literal $minDistance} operation, for use with {@literal $near} or
* {@literal $nearSphere}.
* <p>
* <strong>NOTE:</strong> The unit of measure for distance may depends on the used coordinate representation (legacy
* vs. geoJson) as well as the target operation.
*
* @param minDistance radians or meters
* @return this.
* @since 1.7
*/
@Contract("_ -> this")
public Criteria minDistance(double minDistance) {
if (createNearCriteriaForCommand("$near", "$minDistance", minDistance)
|| createNearCriteriaForCommand("$nearSphere", "$minDistance", minDistance)) {
return this;
}
criteria.put("$minDistance", minDistance);
return this;
}
/**
* Creates a criterion using the {@literal $elemMatch} operator
*
* @param criteria must not be {@literal null}.
* @return this.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/elemMatch/">MongoDB Query operator:
* $elemMatch</a>
*/
@Contract("_ -> this")
public Criteria elemMatch(Criteria criteria) {
this.criteria.put("$elemMatch", criteria.getCriteriaObject());
return this;
}
/**
* Creates a criterion using the given object as a pattern.
*
* @param sample must not be {@literal null}.
* @return this.
* @since 1.8
*/
@Contract("_ -> this")
public Criteria alike(Example<?> sample) {
if (StringUtils.hasText(this.getKey())) {
criteria.put("$example", sample);
return this;
}
Criteria exampleCriteria = new Criteria();
exampleCriteria.criteria.put("$example", sample);
return registerCriteriaChainElement(exampleCriteria);
}
/**
* Creates a criterion ({@code $jsonSchema}) matching documents against a given structure defined by the
* {@link MongoJsonSchema}. <br />
* <strong>NOTE:</strong> {@code $jsonSchema} cannot be used on field/property level but defines the whole document
* structure. Please use
* {@link org.springframework.data.mongodb.core.schema.MongoJsonSchema.MongoJsonSchemaBuilder#properties(JsonSchemaProperty...)}
* to specify nested fields or query them using the {@link #type(Type...) $type} operator.
*
* @param schema must not be {@literal null}.
* @return this
* @since 2.1
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/">MongoDB Query operator:
* $jsonSchema</a>
*/
@Contract("_ -> this")
public Criteria andDocumentStructureMatches(MongoJsonSchema schema) {
Assert.notNull(schema, "Schema must not be null");
Criteria schemaCriteria = new Criteria();
schemaCriteria.criteria.putAll(schema.toDocument());
return registerCriteriaChainElement(schemaCriteria);
}
/**
* Use {@link BitwiseCriteriaOperators} as gateway to create a criterion using one of the
* <a href="https://docs.mongodb.com/manual/reference/operator/query-bitwise/">bitwise operators</a> like
* {@code $bitsAllClear}.
*
* @return new instance of {@link BitwiseCriteriaOperators}. Never {@literal null}.
* @since 2.1
*/
public BitwiseCriteriaOperators bits() {
return new BitwiseCriteriaOperatorsImpl(this);
}
/**
* Creates a criteria using the {@code $or} operator for all provided criteria.
* <p>
* Note that MongoDB doesn't support an {@code $nor} operator to be wrapped in a {@code $not} operator.
*
* @throws IllegalArgumentException if this method follows a {@link #not()} call directly.
* @param criteria must not be {@literal null}.
* @return this.
*/
@Contract("_ -> this")
public Criteria orOperator(Criteria... criteria) {
Assert.notNull(criteria, "Criteria must not be null");
return orOperator(Arrays.asList(criteria));
}
/**
* Creates a criteria using the {@code $or} operator for all of the provided criteria.
* <p>
* Note that MongoDB doesn't support an {@code $nor} operator to be wrapped in a {@code $not} operator.
*
* @throws IllegalArgumentException if this method follows a {@link #not()} call directly.
* @param criteria must not be {@literal null}.
* @return this.
* @since 3.2
*/
@Contract("_ -> this")
public Criteria orOperator(Collection<Criteria> criteria) {
Assert.notNull(criteria, "Criteria must not be null");
BasicDBList bsonList = createCriteriaList(criteria);
return registerCriteriaChainElement(new Criteria("$or").is(bsonList));
}
/**
* Creates a criteria using the {@code $nor} operator for all of the provided criteria.
* <p>
* Note that MongoDB doesn't support an {@code $nor} operator to be wrapped in a {@code $not} operator.
*
* @throws IllegalArgumentException if this method follows a {@link #not()} call directly.
* @param criteria must not be {@literal null}.
* @return this.
*/
@Contract("_ -> this")
public Criteria norOperator(Criteria... criteria) {
Assert.notNull(criteria, "Criteria must not be null");
return norOperator(Arrays.asList(criteria));
}
/**
* Creates a criteria using the {@code $nor} operator for all of the provided criteria.
* <p>
* Note that MongoDB doesn't support an {@code $nor} operator to be wrapped in a {@code $not} operator.
*
* @throws IllegalArgumentException if this method follows a {@link #not()} call directly.
* @param criteria must not be {@literal null}.
* @return this.
* @since 3.2
*/
@Contract("_ -> this")
public Criteria norOperator(Collection<Criteria> criteria) {
Assert.notNull(criteria, "Criteria must not be null");
BasicDBList bsonList = createCriteriaList(criteria);
return registerCriteriaChainElement(new Criteria("$nor").is(bsonList));
}
/**
* Creates a criteria using the {@code $and} operator for all of the provided criteria.
* <p>
* Note that MongoDB doesn't support an {@code $and} operator to be wrapped in a {@code $not} operator.
*
* @throws IllegalArgumentException if this method follows a {@link #not()} call directly.
* @param criteria must not be {@literal null}.
* @return this.
*/
@Contract("_ -> this")
public Criteria andOperator(Criteria... criteria) {
Assert.notNull(criteria, "Criteria must not be null");
return andOperator(Arrays.asList(criteria));
}
/**
* Creates a criteria using the {@code $and} operator for all of the provided criteria.
* <p>
* Note that MongoDB doesn't support an {@code $and} operator to be wrapped in a {@code $not} operator.
*
* @throws IllegalArgumentException if this method follows a {@link #not()} call directly.
* @param criteria must not be {@literal null}.
* @return this.
* @since 3.2
*/
@Contract("_ -> this")
public Criteria andOperator(Collection<Criteria> criteria) {
Assert.notNull(criteria, "Criteria must not be null");
BasicDBList bsonList = createCriteriaList(criteria);
return registerCriteriaChainElement(new Criteria("$and").is(bsonList));
}
/**
* Creates a criterion using the given {@literal operator}.
*
* @param operator the native MongoDB operator.
* @param value the operator value
* @return this.
* @since 5.0
*/
@Contract("_, _ -> this")
public Criteria raw(String operator, Object value) {
criteria.put(operator, value);
return this;
}
private Criteria registerCriteriaChainElement(Criteria criteria) {
if (lastOperatorWasNot()) {
throw new IllegalArgumentException(
"operator $not is not allowed around criteria chain element: " + criteria.getCriteriaObject());
} else {
criteriaChain.add(criteria);
}
return this;
}
/*
* @see org.springframework.data.mongodb.core.query.CriteriaDefinition#getKey()
*/
@Override
public @Nullable String getKey() {
return this.key;
}
@Override
public Document getCriteriaObject() {
if (this.criteriaChain.size() == 1) {
return criteriaChain.get(0).getSingleCriteriaObject();
} else if (CollectionUtils.isEmpty(this.criteriaChain) && !CollectionUtils.isEmpty(this.criteria)) {
return getSingleCriteriaObject();
} else {
Document criteriaObject = new Document();
for (Criteria c : this.criteriaChain) {
Document document = c.getSingleCriteriaObject();
for (String k : document.keySet()) {
Object o = document.get(k);
setValue(criteriaObject, k, o);
}
}
return criteriaObject;
}
}
protected Document getSingleCriteriaObject() {
Document document = new Document();
boolean not = false;
for (Entry<String, Object> entry : criteria.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (requiresGeoJsonFormat(value)) {
value = new Document("$geometry", value);
}
if (not) {
Document notDocument = new Document();
notDocument.put(key, value);
document.put("$not", notDocument);
not = false;
} else {
if ("$not".equals(key) && value == null) {
not = true;
} else {
document.put(key, value);
}
}
}
if (!StringUtils.hasText(this.key)) {
if (not) {
return new Document("$not", document);
}
return document;
}
Document queryCriteria = new Document();
if (!NOT_SET.equals(isValue)) {
if (document.isEmpty()) {
queryCriteria.put(this.key, this.isValue);
} else {
if (isValue instanceof Pattern || isValue instanceof BsonRegularExpression) {
document.put("$regex", isValue);
} else {
document.put("$eq", isValue);
}
queryCriteria.put(this.key, document);
}
} else {
queryCriteria.put(this.key, document);
}
return queryCriteria;
}
private BasicDBList createCriteriaList(Collection<Criteria> criteria) {
BasicDBList bsonList = new BasicDBList();
for (Criteria c : criteria) {
bsonList.add(c.getCriteriaObject());
}
return bsonList;
}
private void setValue(Document document, String key, Object value) {
Object existing = document.get(key);
if (existing == null) {
document.put(key, value);
} else {
throw new InvalidMongoDbApiUsageException("Due to limitations of the org.bson.Document, "
+ "you can't add a second '" + key + "' expression specified as '" + key + " : " + value + "';"
+ " Criteria already contains '" + key + " : " + existing + "'");
}
}
private boolean createNearCriteriaForCommand(String command, String operation, double maxDistance) {
if (!criteria.containsKey(command)) {
return false;
}
Object existingNearOperationValue = criteria.get(command);
if (existingNearOperationValue instanceof Document document) {
document.put(operation, maxDistance);
return true;
} else if (existingNearOperationValue instanceof GeoJson) {
Document dbo = new Document("$geometry", existingNearOperationValue).append(operation, maxDistance);
criteria.put(command, dbo);
return true;
}
return false;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || !getClass().equals(obj.getClass())) {
return false;
}
Criteria that = (Criteria) obj;
if (this.criteriaChain.size() != that.criteriaChain.size()) {
return false;
}
for (int i = 0; i < this.criteriaChain.size(); i++) {
Criteria left = this.criteriaChain.get(i);
Criteria right = that.criteriaChain.get(i);
if (!simpleCriteriaEquals(left, right)) {
return false;
}
}
return true;
}
private boolean simpleCriteriaEquals(Criteria left, Criteria right) {
boolean keyEqual = left.key == null ? right.key == null : left.key.equals(right.key);
boolean criteriaEqual = left.criteria.equals(right.criteria);
boolean valueEqual = isEqual(left.isValue, right.isValue);
return keyEqual && criteriaEqual && valueEqual;
}
/**
* Checks the given objects for equality. Handles {@link Pattern} and arrays correctly.
*
* @param left
* @param right
* @return
*/
private boolean isEqual(@Nullable Object left, @Nullable Object right) {
if (left == null) {
return right == null;
}
if (left instanceof Pattern leftPattern) {
if (!(right instanceof Pattern rightPattern)) {
return false;
}
return leftPattern.pattern().equals(rightPattern.pattern()) //
&& leftPattern.flags() == rightPattern.flags();
}
if (left instanceof Document leftDocument) {
if (!(right instanceof Document rightDocument)) {
return false;
}
Iterator<Entry<String, Object>> leftIterator = leftDocument.entrySet().iterator();
Iterator<Entry<String, Object>> rightIterator = rightDocument.entrySet().iterator();
while (leftIterator.hasNext() && rightIterator.hasNext()) {
Map.Entry<String, Object> leftEntry = leftIterator.next();
Map.Entry<String, Object> rightEntry = rightIterator.next();
if (!isEqual(leftEntry.getKey(), rightEntry.getKey())
|| !isEqual(leftEntry.getValue(), rightEntry.getValue())) {
return false;
}
}
return !leftIterator.hasNext() && !rightIterator.hasNext();
}
if (Collection.class.isAssignableFrom(left.getClass())) {
if (right == null || !Collection.class.isAssignableFrom(right.getClass())) {
return false;
}
Collection<?> leftCollection = (Collection<?>) left;
Collection<?> rightCollection = (Collection<?>) right;
Iterator<?> leftIterator = leftCollection.iterator();
Iterator<?> rightIterator = rightCollection.iterator();
while (leftIterator.hasNext() && rightIterator.hasNext()) {
if (!isEqual(leftIterator.next(), rightIterator.next())) {
return false;
}
}
return !leftIterator.hasNext() && !rightIterator.hasNext();
}
return ObjectUtils.nullSafeEquals(left, right);
}
@Override
public int hashCode() {
int result = 17;
result += nullSafeHashCode(key);
result += criteria.hashCode();
result += nullSafeHashCode(isValue);
return result;
}
private static boolean requiresGeoJsonFormat(Object value) {
return value instanceof GeoJson
|| (value instanceof GeoCommand geoCommand && geoCommand.getShape() instanceof GeoJson);
}
/**
* MongoDB specific <a href="https://docs.mongodb.com/manual/reference/operator/query-bitwise/">bitwise query
* operators</a> like {@code $bitsAllClear, $bitsAllSet,...} for usage with {@link Criteria#bits()} and {@link Query}.
*
* @author Christoph Strobl
* @since 2.1
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/query-bitwise/">https://docs.mongodb.com/manual/reference/operator/query-bitwise/</a>
*/
public interface BitwiseCriteriaOperators {
/**
* Creates a criterion using {@literal $bitsAllClear} matching documents where all given bit positions are clear
* (i.e. 0).
*
* @param numericBitmask non-negative numeric bitmask.
* @return target {@link Criteria}.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAllClear/">MongoDB Query operator:
* $bitsAllClear</a>
* @since 2.1
*/
Criteria allClear(int numericBitmask);
/**
* Creates a criterion using {@literal $bitsAllClear} matching documents where all given bit positions are clear
* (i.e. 0).
*
* @param bitmask string representation of a bitmask that will be converted to its base64 encoded {@link Binary}
* representation. Must not be {@literal null} nor empty.
* @return target {@link Criteria}.
* @throws IllegalArgumentException when bitmask is {@literal null} or empty.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAllClear/">MongoDB Query operator:
* $bitsAllClear</a>
* @since 2.1
*/
Criteria allClear(String bitmask);
/**
* Creates a criterion using {@literal $bitsAllClear} matching documents where all given bit positions are clear
* (i.e. 0).
*
* @param positions list of non-negative integer positions. Positions start at 0 from the least significant bit.
* Must not be {@literal null} nor contain {@literal null} elements.
* @return target {@link Criteria}.
* @throws IllegalArgumentException when positions is {@literal null} or contains {@literal null} elements.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAllClear/">MongoDB Query operator:
* $bitsAllClear</a>
* @since 2.1
*/
Criteria allClear(List<Integer> positions);
/**
* Creates a criterion using {@literal $bitsAllSet} matching documents where all given bit positions are set (i.e.
* 1).
*
* @param numericBitmask non-negative numeric bitmask.
* @return target {@link Criteria}.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAllSet/">MongoDB Query operator:
* $bitsAllSet</a>
* @since 2.1
*/
Criteria allSet(int numericBitmask);
/**
* Creates a criterion using {@literal $bitsAllSet} matching documents where all given bit positions are set (i.e.
* 1).
*
* @param bitmask string representation of a bitmask that will be converted to its base64 encoded {@link Binary}
* representation. Must not be {@literal null} nor empty.
* @return target {@link Criteria}.
* @throws IllegalArgumentException when bitmask is {@literal null} or empty.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAllSet/">MongoDB Query operator:
* $bitsAllSet</a>
* @since 2.1
*/
Criteria allSet(String bitmask);
/**
* Creates a criterion using {@literal $bitsAllSet} matching documents where all given bit positions are set (i.e.
* 1).
*
* @param positions list of non-negative integer positions. Positions start at 0 from the least significant bit.
* Must not be {@literal null} nor contain {@literal null} elements.
* @return target {@link Criteria}.
* @throws IllegalArgumentException when positions is {@literal null} or contains {@literal null} elements.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAllSet/">MongoDB Query operator:
* $bitsAllSet</a>
* @since 2.1
*/
Criteria allSet(List<Integer> positions);
/**
* Creates a criterion using {@literal $bitsAllClear} matching documents where any given bit positions are clear
* (i.e. 0).
*
* @param numericBitmask non-negative numeric bitmask.
* @return target {@link Criteria}.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAnyClear/">MongoDB Query operator:
* $bitsAnyClear</a>
* @since 2.1
*/
Criteria anyClear(int numericBitmask);
/**
* Creates a criterion using {@literal $bitsAllClear} matching documents where any given bit positions are clear
* (i.e. 0).
*
* @param bitmask string representation of a bitmask that will be converted to its base64 encoded {@link Binary}
* representation. Must not be {@literal null} nor empty.
* @return target {@link Criteria}.
* @throws IllegalArgumentException when bitmask is {@literal null} or empty.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAnyClear/">MongoDB Query operator:
* $bitsAnyClear</a>
* @since 2.1
*/
Criteria anyClear(String bitmask);
/**
* Creates a criterion using {@literal $bitsAllClear} matching documents where any given bit positions are clear
* (i.e. 0).
*
* @param positions list of non-negative integer positions. Positions start at 0 from the least significant bit.
* Must not be {@literal null} nor contain {@literal null} elements.
* @return target {@link Criteria}.
* @throws IllegalArgumentException when positions is {@literal null} or contains {@literal null} elements.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAnyClear/">MongoDB Query operator:
* $bitsAnyClear</a>
* @since 2.1
*/
Criteria anyClear(List<Integer> positions);
/**
* Creates a criterion using {@literal $bitsAllSet} matching documents where any given bit positions are set (i.e.
* 1).
*
* @param numericBitmask non-negative numeric bitmask.
* @return target {@link Criteria}.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAnySet/">MongoDB Query operator:
* $bitsAnySet</a>
* @since 2.1
*/
Criteria anySet(int numericBitmask);
/**
* Creates a criterion using {@literal $bitsAnySet} matching documents where any given bit positions are set (i.e.
* 1).
*
* @param bitmask string representation of a bitmask that will be converted to its base64 encoded {@link Binary}
* representation. Must not be {@literal null} nor empty.
* @return target {@link Criteria}.
* @throws IllegalArgumentException when bitmask is {@literal null} or empty.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAnySet/">MongoDB Query operator:
* $bitsAnySet</a>
* @since 2.1
*/
Criteria anySet(String bitmask);
/**
* Creates a criterion using {@literal $bitsAnySet} matching documents where any given bit positions are set (i.e.
* 1).
*
* @param positions list of non-negative integer positions. Positions start at 0 from the least significant bit.
* Must not be {@literal null} nor contain {@literal null} elements.
* @return target {@link Criteria}.
* @throws IllegalArgumentException when positions is {@literal null} or contains {@literal null} elements.
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/bitsAnySet/">MongoDB Query operator:
* $bitsAnySet</a>
* @since 2.1
*/
Criteria anySet(List<Integer> positions);
}
/**
* Default implementation of {@link BitwiseCriteriaOperators}.
*
* @author Christoph Strobl
* @currentRead Beyond the Shadows - Brent Weeks
*/
private static class BitwiseCriteriaOperatorsImpl implements BitwiseCriteriaOperators {
private final Criteria target;
BitwiseCriteriaOperatorsImpl(Criteria target) {
this.target = target;
}
@Override
public Criteria allClear(int numericBitmask) {
return numericBitmask("$bitsAllClear", numericBitmask);
}
@Override
public Criteria allClear(String bitmask) {
return stringBitmask("$bitsAllClear", bitmask);
}
@Override
public Criteria allClear(List<Integer> positions) {
return positions("$bitsAllClear", positions);
}
@Override
public Criteria allSet(int numericBitmask) {
return numericBitmask("$bitsAllSet", numericBitmask);
}
@Override
public Criteria allSet(String bitmask) {
return stringBitmask("$bitsAllSet", bitmask);
}
@Override
public Criteria allSet(List<Integer> positions) {
return positions("$bitsAllSet", positions);
}
@Override
public Criteria anyClear(int numericBitmask) {
return numericBitmask("$bitsAnyClear", numericBitmask);
}
@Override
public Criteria anyClear(String bitmask) {
return stringBitmask("$bitsAnyClear", bitmask);
}
@Override
public Criteria anyClear(List<Integer> positions) {
return positions("$bitsAnyClear", positions);
}
@Override
public Criteria anySet(int numericBitmask) {
return numericBitmask("$bitsAnySet", numericBitmask);
}
@Override
public Criteria anySet(String bitmask) {
return stringBitmask("$bitsAnySet", bitmask);
}
@Override
public Criteria anySet(List<Integer> positions) {
return positions("$bitsAnySet", positions);
}
private Criteria positions(String operator, List<Integer> positions) {
Assert.notNull(positions, "Positions must not be null");
Assert.noNullElements(positions.toArray(), "Positions must not contain null values");
target.criteria.put(operator, positions);
return target;
}
private Criteria stringBitmask(String operator, String bitmask) {
Assert.hasText(bitmask, "Bitmask must not be null");
target.criteria.put(operator, new Binary(Base64.getDecoder().decode(bitmask)));
return target;
}
private Criteria numericBitmask(String operator, int bitmask) {
target.criteria.put(operator, bitmask);
return target;
}
}
}