From 9932be58bf465a5f0e0286691ee1892c3bebcc13 Mon Sep 17 00:00:00 2001 From: Juan Pablo Galeotti Date: Sat, 11 Jul 2026 14:18:39 -0300 Subject: [PATCH] Refactor MongoDB query selectors: use constant operators, add null checks, and improve readability and robustness. Add `QueryParserTest` for comprehensive testing. --- .../mongo/MongoHeuristicsCalculator.java | 82 +- .../java/controller/mongo/QueryParser.java | 18 +- .../mongo/selectors/AllSelector.java | 20 +- .../mongo/selectors/AndSelector.java | 15 +- .../mongo/selectors/ElemMatchSelector.java | 19 +- .../mongo/selectors/EqualsSelector.java | 16 +- .../mongo/selectors/ExistsSelector.java | 18 +- .../selectors/GreaterThanEqualsSelector.java | 15 +- .../mongo/selectors/GreaterThanSelector.java | 16 +- .../mongo/selectors/ImplicitSelector.java | 124 ++- .../mongo/selectors/InSelector.java | 18 +- .../selectors/LessThanEqualsSelector.java | 15 +- .../mongo/selectors/LessThanSelector.java | 15 +- .../mongo/selectors/ModSelector.java | 19 +- .../MultiConditionQuerySelector.java | 67 +- .../mongo/selectors/NearSphereSelector.java | 58 +- .../mongo/selectors/NorSelector.java | 12 +- .../mongo/selectors/NotEqualsSelector.java | 9 +- .../mongo/selectors/NotInSelector.java | 18 +- .../mongo/selectors/NotSelector.java | 41 +- .../mongo/selectors/OrSelector.java | 13 +- .../mongo/selectors/QuerySelector.java | 16 +- .../SingleConditionQuerySelector.java | 47 +- .../mongo/selectors/SizeSelector.java | 19 +- .../mongo/selectors/TypeSelector.java | 7 +- .../controller/mongo/utils/BsonHelper.java | 90 +- .../controller/mongo/QueryParserTest.java | 770 ++++++++++++++++++ 27 files changed, 1386 insertions(+), 191 deletions(-) create mode 100644 client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java index e6c8109fc2..5fcf0ef3c0 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/MongoHeuristicsCalculator.java @@ -20,7 +20,7 @@ public class MongoHeuristicsCalculator { private final TaintHandler taintHandler; public MongoHeuristicsCalculator() { - this(null); + this(null); } public MongoHeuristicsCalculator(TaintHandler taintHandler) { @@ -35,63 +35,65 @@ public MongoHeuristicsCalculator(TaintHandler taintHandler) { * @return a branch distance, where 0 means that the document would make the QUERY resolve as true */ public double computeExpression(Object query, Object doc) { + QueryOperation operation = getOperation(query); - return calculateDistance(operation, doc); + return computeHeuristic(operation, doc); } private QueryOperation getOperation(Object query) { return new QueryParser().parse(query); } - private double calculateDistance(QueryOperation operation, Object doc) { - if (operation instanceof EqualsOperation) + private double computeHeuristic(QueryOperation operation, Object doc) { + if (operation instanceof EqualsOperation) { return calculateDistanceForEquals((EqualsOperation) operation, doc); - if (operation instanceof NotEqualsOperation) + } else if (operation instanceof NotEqualsOperation) { return calculateDistanceForNotEquals((NotEqualsOperation) operation, doc); - if (operation instanceof GreaterThanOperation) + } else if (operation instanceof GreaterThanOperation) { return calculateDistanceForGreaterThan((GreaterThanOperation) operation, doc); - if (operation instanceof GreaterThanEqualsOperation) + } else if (operation instanceof GreaterThanEqualsOperation) { return calculateDistanceForGreaterEqualsThan((GreaterThanEqualsOperation) operation, doc); - if (operation instanceof LessThanOperation) + } else if (operation instanceof LessThanOperation) { return calculateDistanceForLessThan((LessThanOperation) operation, doc); - if (operation instanceof LessThanEqualsOperation) + } else if (operation instanceof LessThanEqualsOperation) { return calculateDistanceForLessEqualsThan((LessThanEqualsOperation) operation, doc); - if (operation instanceof AndOperation) + } else if (operation instanceof AndOperation) { return calculateDistanceForAnd((AndOperation) operation, doc); - if (operation instanceof OrOperation) + } else if (operation instanceof OrOperation) { return calculateDistanceForOr((OrOperation) operation, doc); - if (operation instanceof NorOperation) + } else if (operation instanceof NorOperation) { return calculateDistanceForNor((NorOperation) operation, doc); - if (operation instanceof InOperation) + } else if (operation instanceof InOperation) { return calculateDistanceForIn((InOperation) operation, doc); - if (operation instanceof NotInOperation) + } else if (operation instanceof NotInOperation) { return calculateDistanceForNotIn((NotInOperation) operation, doc); - if (operation instanceof AllOperation) + } else if (operation instanceof AllOperation) { return calculateDistanceForAll((AllOperation) operation, doc); - if (operation instanceof InvertedAllOperation) + } else if (operation instanceof InvertedAllOperation) { return calculateDistanceForInvertedAll((InvertedAllOperation) operation, doc); - if (operation instanceof SizeOperation) + } else if (operation instanceof SizeOperation) { return calculateDistanceForSize((SizeOperation) operation, doc); - if (operation instanceof InvertedSizeOperation) + } else if (operation instanceof InvertedSizeOperation) { return calculateDistanceForInvertedSize((InvertedSizeOperation) operation, doc); - if (operation instanceof ElemMatchOperation) + } else if (operation instanceof ElemMatchOperation) { return calculateDistanceForElemMatch((ElemMatchOperation) operation, doc); - if (operation instanceof ExistsOperation) + } else if (operation instanceof ExistsOperation) { return calculateDistanceForExists((ExistsOperation) operation, doc); - if (operation instanceof ModOperation) + } else if (operation instanceof ModOperation) { return calculateDistanceForMod((ModOperation) operation, doc); - if (operation instanceof InvertedModOperation) + } else if (operation instanceof InvertedModOperation) { return calculateDistanceForInvertedMod((InvertedModOperation) operation, doc); - if (operation instanceof NotOperation) + } else if (operation instanceof NotOperation) { return calculateDistanceForNot((NotOperation) operation, doc); - if (operation instanceof TypeOperation) + } else if (operation instanceof TypeOperation) { return calculateDistanceForType((TypeOperation) operation, doc); - if (operation instanceof InvertedTypeOperation) + } else if (operation instanceof InvertedTypeOperation) { return calculateDistanceForInvertedType((InvertedTypeOperation) operation, doc); - if (operation instanceof NearSphereOperation) + } else if (operation instanceof NearSphereOperation) { return calculateDistanceForNearSphere((NearSphereOperation) operation, doc); - - return Double.MAX_VALUE; + } else { + return Double.MAX_VALUE; + } } private double calculateDistanceForEquals(EqualsOperation operation, Object doc) { @@ -134,7 +136,7 @@ private double calculateDistanceForComparisonOperation(ComparisonOperation op private double calculateDistanceForOr(OrOperation operation, Object doc) { return operation.getConditions().stream() - .mapToDouble(condition -> calculateDistance(condition, doc)) + .mapToDouble(condition -> computeHeuristic(condition, doc)) .min() .getAsDouble(); } @@ -143,7 +145,7 @@ private double calculateDistanceForAnd(AndOperation operation, Object doc) { return operation.getConditions() .stream() .mapToDouble(condition -> - TruthnessUtils.normalizeValue(calculateDistance(condition, doc))) + TruthnessUtils.normalizeValue(computeHeuristic(condition, doc))) .sum(); } @@ -233,7 +235,7 @@ private double calculateDistanceForElemMatch(ElemMatchOperation operation, Objec .mapToDouble(elem -> { Object newDoc = newDocument(doc); appendToDocument(newDoc, operation.getFieldName(), elem); - return calculateDistance(operation.getCondition(), newDoc); + return computeHeuristic(operation.getCondition(), newDoc); }) .min() .getAsDouble(); @@ -289,14 +291,14 @@ private double calculateDistanceForNot(NotOperation operation, Object doc) { QueryOperation condition = operation.getCondition(); QueryOperation invertedOperation = invertOperation(condition); - return calculateDistance(invertedOperation, doc); + return computeHeuristic(invertedOperation, doc); } private double calculateDistanceForNor(NorOperation operation, Object doc) { return operation.getConditions() .stream() .mapToDouble(condition -> - TruthnessUtils.normalizeValue(calculateDistance(invertOperation(condition), doc))) + TruthnessUtils.normalizeValue(computeHeuristic(invertOperation(condition), doc))) .sum(); } @@ -332,7 +334,7 @@ private double calculateDistanceForNearSphere(NearSphereOperation operation, Obj type key is case-sensitive. (https://datatracker.ietf.org/doc/html/rfc7946#section-1.4) for more details. */ - if (isDocument(actualPoint) && getValue(actualPoint, "type").equals("Point") && getValue(actualPoint, "coordinates") instanceof List) { + if (isBsonDocument(actualPoint) && getValue(actualPoint, "type").equals("Point") && getValue(actualPoint, "coordinates") instanceof List) { List coordinates = (List) getValue(actualPoint, "coordinates"); x2 = Math.toRadians((Double) coordinates.get(0)); @@ -465,8 +467,8 @@ private double compareValues(Object val1, Object val2) { if (val1 instanceof String && val2 instanceof String) { - if(taintHandler!=null){ - taintHandler.handleTaintForStringEquals((String)val1,(String)val2, false); + if (taintHandler != null) { + taintHandler.handleTaintForStringEquals((String) val1, (String) val2, false); } return (double) DistanceHelper.getLeftAlignmentDistance((String) val1, (String) val2); @@ -477,15 +479,15 @@ private double compareValues(Object val1, Object val2) { } if (val1 instanceof String && isObjectId(val2)) { - if(taintHandler!=null){ - taintHandler.handleTaintForStringEquals((String)val1,val2.toString(),false); + if (taintHandler != null) { + taintHandler.handleTaintForStringEquals((String) val1, val2.toString(), false); } return (double) DistanceHelper.getLeftAlignmentDistance((String) val1, val2.toString()); } if (val2 instanceof String && isObjectId(val1)) { - if(taintHandler!=null){ - taintHandler.handleTaintForStringEquals(val1.toString(),val2.toString(),false); + if (taintHandler != null) { + taintHandler.handleTaintForStringEquals(val1.toString(), val2.toString(), false); } return (double) DistanceHelper.getLeftAlignmentDistance(val1.toString(), (String) val2); } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/QueryParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/QueryParser.java index cfd3725ea2..2ac565f6a4 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/QueryParser.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/QueryParser.java @@ -6,6 +6,7 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; /** * Determines to which operation a query correspond. @@ -35,11 +36,16 @@ public class QueryParser { new ImplicitSelector() ); - public QueryOperation parse(Object query) { - return selectors.stream() - .map(selector -> selector.getOperation(query)) + public QueryOperation parse(Object bsonDocument) { + List results = selectors.stream() + .map(selector -> selector.getOperation(bsonDocument)) .filter(Objects::nonNull) - .findFirst() - .orElse(null); + .collect(Collectors.toList()); + + if (results.size() != 1) { + return null; + } + + return results.get(0); } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/AllSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/AllSelector.java index 921b260800..80fa06bd01 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/AllSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/AllSelector.java @@ -3,21 +3,35 @@ import org.evomaster.client.java.controller.mongo.operations.*; import java.util.List; +import java.util.Objects; /** + * Represents a selector for the MongoDB `$all` operator. * { field: { $all: [ value1 , value2 ... ] } } + * + *

+ * This operator matches arrays that contain all elements specified in the query. + * The selector checks if the query value is a list and, if so, creates an + * {@link AllOperation} object corresponding to the field and the list of values. */ public class AllSelector extends SingleConditionQuerySelector { + + public static final String ALL_OPERATOR = "$all"; + @Override protected QueryOperation parseValue(String fieldName, Object value) { + Objects.requireNonNull(fieldName); + Objects.requireNonNull(value); + if (value instanceof List) { return new AllOperation<>(fieldName, (List) value); + } else { + return null; } - return null; } @Override protected String operator() { - return "$all"; + return ALL_OPERATOR; } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/AndSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/AndSelector.java index 111b35a244..d5a45d4ace 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/AndSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/AndSelector.java @@ -5,21 +5,24 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** + * Represents a selector for the MongoDB `$and` operator. * { $and: [ { expression1 }, { expression2 } , ... , { expressionN } ] } */ public class AndSelector extends MultiConditionQuerySelector { + public static final String AND_OPERATOR = "$and"; + @Override - protected QueryOperation parseConditions(List value) { - ArrayList conditions = new ArrayList<>(); - value.forEach(condition -> conditions.add(new QueryParser().parse(condition))); - return conditions.isEmpty()? null : new AndOperation(conditions); + protected QueryOperation composeConditions(List conditions) { + Objects.requireNonNull(conditions); + return conditions.isEmpty() ? null : new AndOperation(conditions); } @Override protected String operator() { - return "$and"; + return AND_OPERATOR; } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ElemMatchSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ElemMatchSelector.java index d0228adb64..82bdcb07e3 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ElemMatchSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ElemMatchSelector.java @@ -3,22 +3,31 @@ import org.evomaster.client.java.controller.mongo.operations.*; import org.evomaster.client.java.controller.mongo.QueryParser; -import static org.evomaster.client.java.controller.mongo.utils.BsonHelper.isDocument; +import java.util.Objects; + +import static org.evomaster.client.java.controller.mongo.utils.BsonHelper.isBsonDocument; /** * { field: { $elemMatch: { query1, query2, ... } } } */ public class ElemMatchSelector extends SingleConditionQuerySelector { + + public static final String ELEM_MATCH_OPERATOR = "$elemMatch"; + @Override protected QueryOperation parseValue(String fieldName, Object value) { - if (isDocument(value)) { + Objects.requireNonNull(fieldName); + Objects.requireNonNull(value); + + if (isBsonDocument(value)) { QueryOperation condition = new QueryParser().parse(value); return new ElemMatchOperation(fieldName, condition); + } else { + return null; } - return null; } @Override protected String operator() { - return "$elemMatch"; + return ELEM_MATCH_OPERATOR; } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/EqualsSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/EqualsSelector.java index ed44b3d50e..371dac9586 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/EqualsSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/EqualsSelector.java @@ -3,18 +3,30 @@ import org.evomaster.client.java.controller.mongo.operations.EqualsOperation; import org.evomaster.client.java.controller.mongo.operations.QueryOperation; +import java.util.Objects; + /** * { field: { $eq: value } } */ public class EqualsSelector extends SingleConditionQuerySelector { + public static final String EQUALS_OPERATOR = "$eq"; + + /** + * Parses the provided field name and value into a QueryOperation representing an equality condition. + * + * @param fieldName the name of the field involved in the condition; must not be null + * @param value the value to match against the field; null is a valid value + * @return an instance of EqualsOperation representing the equality condition for the given field and value + */ @Override protected QueryOperation parseValue(String fieldName, Object value) { + Objects.requireNonNull(fieldName); return new EqualsOperation<>(fieldName, value); } @Override protected String operator() { - return "$eq"; + return EQUALS_OPERATOR; } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ExistsSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ExistsSelector.java index d51167e60c..708cf75984 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ExistsSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ExistsSelector.java @@ -2,18 +2,28 @@ import org.evomaster.client.java.controller.mongo.operations.*; +import java.util.Objects; + /** * { field: { $exists: boolean } } */ public class ExistsSelector extends SingleConditionQuerySelector { + + public static final String EXISTS_OPERATOR = "$exists"; + @Override protected QueryOperation parseValue(String fieldName, Object value) { - if (value instanceof Boolean) return new ExistsOperation(fieldName, (Boolean) value); - return null; + Objects.requireNonNull(fieldName); + + if (value!=null && value instanceof Boolean) { + return new ExistsOperation(fieldName, (Boolean) value); + } else { + return null; + } } @Override protected String operator() { - return "$exists"; + return EXISTS_OPERATOR; } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/GreaterThanEqualsSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/GreaterThanEqualsSelector.java index 99a6908073..0c2bca1f34 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/GreaterThanEqualsSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/GreaterThanEqualsSelector.java @@ -3,18 +3,27 @@ import org.evomaster.client.java.controller.mongo.operations.GreaterThanEqualsOperation; import org.evomaster.client.java.controller.mongo.operations.QueryOperation; +import java.util.Objects; + /** * { field: { $gte: value } } */ public class GreaterThanEqualsSelector extends SingleConditionQuerySelector { + public static final String GREATER_THAN_EQUALS_OPERATOR = "$gte"; + @Override protected QueryOperation parseValue(String fieldName, Object value) { - return new GreaterThanEqualsOperation<>(fieldName, value); + Objects.requireNonNull(fieldName); + if (value==null) { + return null; + } else { + return new GreaterThanEqualsOperation<>(fieldName, value); + } } @Override protected String operator() { - return "$gte"; + return GREATER_THAN_EQUALS_OPERATOR; } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/GreaterThanSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/GreaterThanSelector.java index 2709b24bd4..f6ea4f2b07 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/GreaterThanSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/GreaterThanSelector.java @@ -3,18 +3,28 @@ import org.evomaster.client.java.controller.mongo.operations.GreaterThanOperation; import org.evomaster.client.java.controller.mongo.operations.QueryOperation; +import java.util.Objects; + /** * { field: { $gt: value } } */ public class GreaterThanSelector extends SingleConditionQuerySelector { + public static final String GREATER_THAN_OPERATOR = "$gt"; + @Override protected QueryOperation parseValue(String fieldName, Object value) { - return new GreaterThanOperation<>(fieldName, value); + Objects.requireNonNull(fieldName); + + if (value == null) { + return null; + } else { + return new GreaterThanOperation<>(fieldName, value); + } } @Override protected String operator() { - return "$gt"; + return GREATER_THAN_OPERATOR; } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ImplicitSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ImplicitSelector.java index f04f82bdca..0907a07119 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ImplicitSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ImplicitSelector.java @@ -16,25 +16,127 @@ * And: {{ expression1 }, { expression2 } , ... , { expressionN } } */ public class ImplicitSelector extends QuerySelector { + + private static final String PREFIX_OPERATOR = "$"; + @Override public QueryOperation getOperation(Object query) { + if (!isImplicitQuery(query)) { + return null; + } + if (!isUniqueEntry((Map) query)) { - List conditions = parseConditions(query); - return conditions.isEmpty()? null : new AndOperation(conditions); - }else{ - String fieldName = extractFieldName(query); - return new EqualsOperation<>(fieldName, getValue(query, fieldName)); + return handleMultipleFields(query); + } else { + return handleSingleField(query); + } + } + + private boolean isImplicitQuery(Object query) { + if (!(query instanceof Map)) { + return false; + } + + Set keys = documentKeys(query); + if (keys == null || keys.isEmpty()) { + return false; + } + + // If any key starts with $, this is not an implicit query + return keys.stream().noneMatch(k -> k.startsWith(PREFIX_OPERATOR)); + } + + private QueryOperation handleMultipleFields(Object query) { + List conditions = parseConditions(query); + return (conditions == null || conditions.isEmpty()) ? null : new AndOperation(conditions); + } + + private QueryOperation handleSingleField(Object query) { + String fieldName = extractFieldName(query); + Object value = getValue(query, fieldName); + + // If the value is a document, it might contain multiple operators (e.g. {age: {$gte: 18, $lt: 65}}) + // or it might be a literal document to match (e.g. {metadata: {foo: "bar"}}) + if (isBsonDocument(value)) { + if (isEmptyDocument(value)) { + return null; + } + QueryOperation multiOperatorOp = handlePotentialMultiOperatorValue(query, fieldName, value); + if (multiOperatorOp != null || isBsonDocumentWithOperators(value)) { + return multiOperatorOp; + } } + return new EqualsOperation<>(fieldName, value); + } + + private boolean isEmptyDocument(Object value) { + Set keys = documentKeys(value); + return keys == null || keys.isEmpty(); + } + + private QueryOperation handlePotentialMultiOperatorValue(Object query, String fieldName, Object value) { + Set innerKeys = documentKeys(value); + if (innerKeys == null || innerKeys.isEmpty()) { + return null; + } + + // If it contains operators, they must all be operators + boolean hasOperators = innerKeys.stream().anyMatch(k -> k.startsWith(PREFIX_OPERATOR)); + boolean allOperators = innerKeys.stream().allMatch(k -> k.startsWith(PREFIX_OPERATOR)); + + if (hasOperators) { + if (!allOperators) { + return null; // Mixed operators and fields are not allowed in this position + } + if (innerKeys.size() > 1) { + return handleMultiOperatorField(query, fieldName, value, innerKeys); + } + // If it's a single operator, let other selectors handle it (return null here) + return null; + } + return null; + } + + private boolean isBsonDocumentWithOperators(Object value) { + if (!isBsonDocument(value)) { + return false; + } + Set keys = documentKeys(value); + return keys != null && keys.stream().anyMatch(k -> k.startsWith(PREFIX_OPERATOR)); + } + + private QueryOperation handleMultiOperatorField(Object query, String fieldName, Object value, Set operators) { + // Multiple operators on the same field: { age: { $gte: 18, $lt: 65 } } + // Split into multiple conditions: { age: { $gte: 18 } } and { age: { $lt: 65 } } + List conditions = new ArrayList<>(); + for (String operator : operators) { + Object operatorValue = getValue(value, operator); + Object newQuery = newDocument(query); + Object newInnerDoc = newDocument(value); + appendToDocument(newInnerDoc, operator, operatorValue); + appendToDocument(newQuery, fieldName, newInnerDoc); + QueryOperation operation = new QueryParser().parse(newQuery); + if (operation == null) { + return null; + } + conditions.add(operation); + } + return new AndOperation(conditions); } protected List parseConditions(Object query) { - Set fields = keySet(query); + Set fields = documentKeys(query); ArrayList conditions = new ArrayList<>(); - fields.forEach(fieldName -> { + if (fields == null) return conditions; + for (String fieldName : fields) { Object newQuery = newDocument(query); appendToDocument(newQuery, fieldName, getValue(query, fieldName)); - conditions.add(new QueryParser().parse(newQuery)); - }); + QueryOperation operation = new QueryParser().parse(newQuery); + if (operation == null) { + return null; // All parts of an implicit AND must be valid + } + conditions.add(operation); + } return conditions; } @@ -49,6 +151,6 @@ protected String operator() { private String extractFieldName(Object query) { Set keys = documentKeys(query); - return keys.stream().findFirst().orElse(null); + return keys == null ? null : keys.stream().findFirst().orElse(null); } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/InSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/InSelector.java index 1a8ad7fb64..4836dd4001 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/InSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/InSelector.java @@ -3,19 +3,29 @@ import org.evomaster.client.java.controller.mongo.operations.*; import java.util.List; +import java.util.Objects; /** * { field: { $in: [value1, value2, ... valueN ] } } */ public class InSelector extends SingleConditionQuerySelector { + + public static final String IN_OPERATOR = "$in"; + @Override protected QueryOperation parseValue(String fieldName, Object value) { - if (value instanceof List) return new InOperation<>(fieldName, (List) value); - return null; + Objects.requireNonNull(fieldName); + Objects.requireNonNull(value); + + if (value instanceof List) { + return new InOperation<>(fieldName, (List) value); + } else { + return null; + } } @Override protected String operator() { - return "$in"; + return IN_OPERATOR; } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/LessThanEqualsSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/LessThanEqualsSelector.java index 48cf09128c..e97608da29 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/LessThanEqualsSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/LessThanEqualsSelector.java @@ -3,18 +3,27 @@ import org.evomaster.client.java.controller.mongo.operations.LessThanEqualsOperation; import org.evomaster.client.java.controller.mongo.operations.QueryOperation; +import java.util.Objects; + /** * { field: { $lte: value } } */ public class LessThanEqualsSelector extends SingleConditionQuerySelector { + public static final String LESS_THAN_EQUALS = "$lte"; + @Override protected QueryOperation parseValue(String fieldName, Object value) { - return new LessThanEqualsOperation<>(fieldName, value); + Objects.requireNonNull(fieldName); + if (value==null) { + return null; + } else { + return new LessThanEqualsOperation<>(fieldName, value); + } } @Override protected String operator() { - return "$lte"; + return LESS_THAN_EQUALS; } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/LessThanSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/LessThanSelector.java index fef86cd9e9..ab774058be 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/LessThanSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/LessThanSelector.java @@ -3,18 +3,27 @@ import org.evomaster.client.java.controller.mongo.operations.LessThanOperation; import org.evomaster.client.java.controller.mongo.operations.QueryOperation; +import java.util.Objects; + /** * { field: { $lt: value } } */ public class LessThanSelector extends SingleConditionQuerySelector { + public static final String LESS_THAN_OPERATOR = "$lt"; + @Override protected QueryOperation parseValue(String fieldName, Object value) { - return new LessThanOperation<>(fieldName, value); + Objects.requireNonNull(fieldName); + if (value==null) { + return null; + } else { + return new LessThanOperation<>(fieldName, value); + } } @Override protected String operator() { - return "$lt"; + return LESS_THAN_OPERATOR; } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ModSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ModSelector.java index 1b875a35e3..5d6639922d 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ModSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/ModSelector.java @@ -3,23 +3,32 @@ import org.evomaster.client.java.controller.mongo.operations.*; import java.util.List; +import java.util.Objects; /** * { field: { $mod: [ divisor, remainder ] } } */ public class ModSelector extends SingleConditionQuerySelector { + + public static final String MOD_OPERATOR = "$mod"; + @Override protected QueryOperation parseValue(String fieldName, Object value) { + Objects.requireNonNull(fieldName); + Objects.requireNonNull(value); + if (value instanceof List) { - Long divisor = (Long) ((List) value).get(0); - Long remainder = (Long) ((List) value).get(1); + List listOfValues = (List) value; + Long divisor = (Long) listOfValues.get(0); + Long remainder = (Long) listOfValues.get(1); return new ModOperation(fieldName, divisor, remainder); + } else { + return null; } - return null; } @Override protected String operator() { - return "$mod"; + return MOD_OPERATOR; } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/MultiConditionQuerySelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/MultiConditionQuerySelector.java index d76543c962..435988cc17 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/MultiConditionQuerySelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/MultiConditionQuerySelector.java @@ -1,28 +1,73 @@ package org.evomaster.client.java.controller.mongo.selectors; +import org.evomaster.client.java.controller.mongo.QueryParser; import org.evomaster.client.java.controller.mongo.operations.*; -import java.util.List; -import java.util.Map; +import java.util.*; -import static org.evomaster.client.java.controller.mongo.utils.BsonHelper.getValue; -import static org.evomaster.client.java.controller.mongo.utils.BsonHelper.documentKeys; +import static org.evomaster.client.java.controller.mongo.utils.BsonHelper.*; /** * Selectors for operations whose value consist of a list of conditions */ abstract public class MultiConditionQuerySelector extends QuerySelector { @Override - public QueryOperation getOperation(Object query) { - if (!isUniqueEntry((Map) query) || !hasTheExpectedOperator(query)) return null; - Object value = getValue(query, operator()); - return (value instanceof List)? parseConditions((List) value) : null; + public QueryOperation getOperation(Object bsonDocument) { + Objects.requireNonNull(bsonDocument); + + if (!hasTheExpectedOperator(bsonDocument)) { + return null; + } + + Object value = getValue(bsonDocument, operator()); + if (value == null) { + return null; + } + if (!(value instanceof List)) { + return null; + } + + ArrayList conditions = parseConditions((List) value); + if (conditions == null) { + return null; + } else { + return composeConditions(conditions); + } } @Override protected String extractOperator(Object query) { - return documentKeys(query).stream().findFirst().orElse(null); + Objects.requireNonNull(query); + if (!isBsonDocument(query)) { + return null; + } else { + Set keys = documentKeys(query); + return keys == null ? null : keys.stream().findFirst().orElse(null); + } } - protected abstract QueryOperation parseConditions(List value); -} \ No newline at end of file + private static ArrayList parseConditions(List value) { + ArrayList conditions = new ArrayList<>(); + for (Object condition : value) { + QueryOperation operation = new QueryParser().parse(condition); + if (operation == null) { + return null; + } + conditions.add(operation); + } + return conditions; + } + + /** + * Composes a {@link QueryOperation} from a list of conditions. The specific behavior + * of this method depends on the implementation in subclasses and how they process + * the provided conditions to create a composite operation. + * + * @param conditions the list of {@link QueryOperation} objects representing the individual query conditions + * @return a {@link QueryOperation} representing the composed query based on the given conditions, + * or null if the conditions cannot be composed + */ + protected abstract QueryOperation composeConditions(List conditions); + + +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NearSphereSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NearSphereSelector.java index 2f6eae1490..b6863437e3 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NearSphereSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NearSphereSelector.java @@ -4,6 +4,7 @@ import org.evomaster.client.java.controller.mongo.operations.QueryOperation; import java.util.List; +import java.util.Objects; import java.util.Set; import static org.evomaster.client.java.controller.mongo.utils.BsonHelper.*; @@ -16,16 +17,32 @@ public class NearSphereSelector extends QuerySelector { public static final int EARTH_RADIUS_IN_METERS = 6371000; + private static final String MIN_DISTANCE_OPERATOR = "$minDistance"; + private static final String MAX_DISTANCE_OPERATOR = "$maxDistance"; + private static final String GEOMETRY_OPERATOR = "$geometry"; + private static final String NEAR_SPHERE_OPERATOR = "$nearSphere"; + private static final String COORDINATES_FIELD_NAME = "coordinates"; + private static final String X_FIELD_NAME = "x"; + private static final String Y_FIELD_NAME = "y"; @Override public QueryOperation getOperation(Object query) { String fieldName = extractFieldName(query); + if (fieldName == null) { + return null; + } Object innerDoc = getValue(query, fieldName); - if (!isDocument(innerDoc) || !hasTheExpectedOperator(query)) return null; + if (!isBsonDocument(innerDoc) || !hasTheExpectedOperator(query)) { + return null; + } Object point = getValue(innerDoc, operator()); - Object geometry = getValue(point, "$geometry"); + if (point == null) { + return null; + } + + Object geometry = isBsonDocument(point) ? getValue(point, GEOMETRY_OPERATOR) : null; boolean legacyCoordinates = geometry == null; return parseValue(fieldName, innerDoc, legacyCoordinates); @@ -33,13 +50,16 @@ public QueryOperation getOperation(Object query) { protected String extractOperator(Object query) { String fieldName = extractFieldName(query); + if (fieldName == null) { + return null; + } Set keys = documentKeys(getValue(query, fieldName)); - return keys.stream().findFirst().orElse(null); + return keys == null ? null : keys.stream().findFirst().orElse(null); } @Override protected String operator() { - return "$nearSphere"; + return NEAR_SPHERE_OPERATOR; } public QueryOperation parseValue(String fieldName, Object innerDoc, boolean legacyCoordinates) { @@ -51,8 +71,8 @@ public QueryOperation parseValue(String fieldName, Object innerDoc, boolean lega Object point = getValue(innerDoc, operator()); if (legacyCoordinates) { - Object maxDistanceInRadians = getValue(innerDoc, "$maxDistance"); - Object minDistanceInRadians = getValue(innerDoc, "$minDistance"); + Object maxDistanceInRadians = getValue(innerDoc, MAX_DISTANCE_OPERATOR); + Object minDistanceInRadians = getValue(innerDoc, MIN_DISTANCE_OPERATOR); if (maxDistanceInRadians instanceof Double) { maxDistance = radiansToMeters((double) maxDistanceInRadians); @@ -62,11 +82,11 @@ public QueryOperation parseValue(String fieldName, Object innerDoc, boolean lega minDistance = radiansToMeters((double) minDistanceInRadians); } - longitude = getValue(point, "x"); - latitude = getValue(point, "y"); + longitude = getValue(point, X_FIELD_NAME); + latitude = getValue(point, Y_FIELD_NAME); } else { - Object geometry = getValue(point, "$geometry"); - Object coordinates = getValue(geometry, "coordinates"); + Object geometry = getValue(point, GEOMETRY_OPERATOR); + Object coordinates = getValue(geometry, COORDINATES_FIELD_NAME); if (coordinates instanceof List && ((List) coordinates).size() == 2) { longitude = ((List) coordinates).get(0); @@ -75,14 +95,15 @@ public QueryOperation parseValue(String fieldName, Object innerDoc, boolean lega return null; } - maxDistance = getValue(point, "$maxDistance"); - minDistance = getValue(point, "$minDistance"); + maxDistance = getValue(point, MAX_DISTANCE_OPERATOR); + minDistance = getValue(point, MIN_DISTANCE_OPERATOR); } if (longitude instanceof Double && latitude instanceof Double && (maxDistance == null || maxDistance instanceof Double) && (minDistance == null || minDistance instanceof Double)) { return new NearSphereOperation(fieldName, (Double) longitude, (Double) latitude, (Double) maxDistance, (Double) minDistance); + } else { + return null; } - return null; } private static double radiansToMeters(double radians) { @@ -90,7 +111,12 @@ private static double radiansToMeters(double radians) { } private String extractFieldName(Object query) { - Set keys = documentKeys(query); - return keys.stream().findFirst().orElse(null); + Objects.requireNonNull(query); + if (!isBsonDocument(query)) { + return null; + } else { + Set keys = documentKeys(query); + return keys == null ? null : keys.stream().findFirst().orElse(null); + } } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NorSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NorSelector.java index 98921e5316..0eaf19f722 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NorSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NorSelector.java @@ -5,21 +5,23 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * { $nor: [ { expression1 }, { expression2 }, ... { expressionN } ] } */ public class NorSelector extends MultiConditionQuerySelector { + public static final String NOR_OPERATOR = "$nor"; + @Override - protected QueryOperation parseConditions(List value) { - ArrayList conditions = new ArrayList<>(); - value.forEach(condition -> conditions.add(new QueryParser().parse(condition))); + protected QueryOperation composeConditions(List conditions) { + Objects.requireNonNull(conditions); return conditions.isEmpty()? null : new NorOperation(conditions); } @Override protected String operator() { - return "$nor"; + return NOR_OPERATOR; } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NotEqualsSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NotEqualsSelector.java index 97e6fd1d75..d9e3ad0aa5 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NotEqualsSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NotEqualsSelector.java @@ -3,18 +3,23 @@ import org.evomaster.client.java.controller.mongo.operations.NotEqualsOperation; import org.evomaster.client.java.controller.mongo.operations.QueryOperation; +import java.util.Objects; + /** * { field: { $ne: value } } */ public class NotEqualsSelector extends SingleConditionQuerySelector { + public static final String NOT_EQUALS_OPERATOR = "$ne"; + @Override protected QueryOperation parseValue(String fieldName, Object value) { + Objects.requireNonNull(fieldName); return new NotEqualsOperation<>(fieldName, value); } @Override protected String operator() { - return "$ne"; + return NOT_EQUALS_OPERATOR; } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NotInSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NotInSelector.java index f8e1823ec7..2ea575b7d3 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NotInSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NotInSelector.java @@ -3,19 +3,29 @@ import org.evomaster.client.java.controller.mongo.operations.*; import java.util.List; +import java.util.Objects; /** * { field: { $nin: [ value1, value2 ... valueN ] } } */ public class NotInSelector extends SingleConditionQuerySelector { + + public static final String NOT_IN_OPERATOR = "$nin"; + @Override protected QueryOperation parseValue(String fieldName, Object value) { - if (value instanceof List) return new NotInOperation<>(fieldName, (List) value); - return null; + Objects.requireNonNull(fieldName); + Objects.requireNonNull(value); + + if (value instanceof List) { + return new NotInOperation<>(fieldName, (List) value); + } else { + return null; + } } @Override protected String operator() { - return "$nin"; + return NOT_IN_OPERATOR; } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NotSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NotSelector.java index ad2a79c739..06e52a515a 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NotSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/NotSelector.java @@ -3,27 +3,58 @@ import org.evomaster.client.java.controller.mongo.operations.*; import org.evomaster.client.java.controller.mongo.QueryParser; +import java.util.Set; + +import static org.evomaster.client.java.controller.mongo.selectors.AndSelector.AND_OPERATOR; +import static org.evomaster.client.java.controller.mongo.selectors.OrSelector.OR_OPERATOR; +import static org.evomaster.client.java.controller.mongo.selectors.NorSelector.NOR_OPERATOR; + import static org.evomaster.client.java.controller.mongo.utils.BsonHelper.*; /** * { field: { $not: { operator-expression } } } */ public class NotSelector extends SingleConditionQuerySelector { + + public static final String NOT_OPERATOR = "$not"; + @Override protected QueryOperation parseValue(String fieldName, Object value) { - if (isDocument(value)) { + if (isBsonDocument(value)) { + // A $not cannot contain $and, $or, $nor, etc. (logical operators at field level) + // or other $not + Set keys = documentKeys(value); + if (keys == null || keys.isEmpty()) { + return null; + } + String innerOp = keys.iterator().next(); + if (innerOp.equals(NOT_OPERATOR) + || innerOp.equals(AND_OPERATOR) + || innerOp.equals(OR_OPERATOR) + || innerOp.equals(NOR_OPERATOR)) { + return null; + } + // This is necessary for query parser to work correctly as the syntax for not is different // The field is at the beginning instead Object docWithRemovedNot = newDocument(value); appendToDocument(docWithRemovedNot, fieldName, value); QueryOperation condition = new QueryParser().parse(docWithRemovedNot); - return new NotOperation(fieldName, condition); + + if (condition == null || condition instanceof AndOperation) { + // If it parsed as AndOperation (because of ImplicitSelector), it's probably wrong here + // or if it failed to parse. + return null; + } else { + return new NotOperation(fieldName, condition); + } + } else { + return null; } - return null; } @Override protected String operator() { - return "$not"; + return NOT_OPERATOR; } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/OrSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/OrSelector.java index 2dee66890d..83d23eb409 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/OrSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/OrSelector.java @@ -5,20 +5,23 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * { $or: [ { expression1 }, { expression2 }, ... , { expressionN } ] } */ public class OrSelector extends MultiConditionQuerySelector { + + public static final String OR_OPERATOR = "$or"; + @Override - protected QueryOperation parseConditions(List value) { - ArrayList conditions = new ArrayList<>(); - value.forEach(condition -> conditions.add(new QueryParser().parse(condition))); + protected QueryOperation composeConditions(List conditions) { + Objects.requireNonNull(conditions); return conditions.isEmpty()? null : new OrOperation(conditions); } @Override protected String operator() { - return "$or"; + return OR_OPERATOR; } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/QuerySelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/QuerySelector.java index 1c56b62cb8..81510e583c 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/QuerySelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/QuerySelector.java @@ -15,13 +15,25 @@ abstract public class QuerySelector { */ public abstract QueryOperation getOperation(Object query); + /** + * Determines if the given map contains exactly one entry. + * + * @param map the map to be checked for uniqueness of entries; must not be null + * @return true if the map contains exactly one entry; false otherwise + */ protected Boolean isUniqueEntry(Map map) { return map.size() == 1; } + /** + * Determines if the given query has the expected operator associated with the selector. + * + * @param query the query object whose operator needs to be checked; must not be null + * @return true if the query contains the expected operator; false otherwise + */ protected Boolean hasTheExpectedOperator(Object query) { String actualOperator = extractOperator(query); - return actualOperator.equals(operator()); + return actualOperator != null && actualOperator.equals(operator()); } /** @@ -33,4 +45,4 @@ protected Boolean hasTheExpectedOperator(Object query) { * The operator a query must have to be considered as an instance of the corresponding operation. */ protected abstract String operator(); -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/SingleConditionQuerySelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/SingleConditionQuerySelector.java index 1297e27964..126b78c048 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/SingleConditionQuerySelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/SingleConditionQuerySelector.java @@ -3,6 +3,7 @@ import org.evomaster.client.java.controller.mongo.operations.*; import java.util.Map; +import java.util.Objects; import java.util.Set; import static org.evomaster.client.java.controller.mongo.utils.BsonHelper.*; @@ -12,24 +13,52 @@ */ abstract class SingleConditionQuerySelector extends QuerySelector { @Override - public QueryOperation getOperation(Object query) { - String fieldName = extractFieldName(query); - Object innerDoc = getValue(query, fieldName); - if (!isUniqueEntry((Map) query) || !isDocument(innerDoc) || !hasTheExpectedOperator(query)) return null; - Object value = getValue(innerDoc, operator()); - return parseValue(fieldName, value); + public QueryOperation getOperation(Object bsonDocument) { + Objects.requireNonNull(bsonDocument); + + if (!(bsonDocument instanceof Map)) { + return null; + } + + String fieldName = extractFieldName(bsonDocument); + if (fieldName == null) { + return null; + } + + if (!isUniqueEntry((Map) bsonDocument)) { + return null; + } + + Object innerDoc = getValue(bsonDocument, fieldName); + if (!isBsonDocument(innerDoc) || !hasTheExpectedOperator(bsonDocument)) { + return null; + } + + Set innerKeys = documentKeys(innerDoc); + if (innerKeys == null || innerKeys.size() != 1) { + return null; + } + + if (innerKeys.contains(operator())) { + Object value = getValue(innerDoc, operator()); + return parseValue(fieldName, value); + } else { + return null; + } + } protected String extractOperator(Object query) { String fieldName = extractFieldName(query); + if (fieldName == null) return null; Set keys = documentKeys(getValue(query, fieldName)); - return keys.stream().findFirst().orElse(null); + return keys == null ? null : keys.stream().findFirst().orElse(null); } protected abstract QueryOperation parseValue(String fieldName, Object value); private String extractFieldName(Object query) { Set keys = documentKeys(query); - return keys.stream().findFirst().orElse(null); + return keys == null ? null : keys.stream().findFirst().orElse(null); } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/SizeSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/SizeSelector.java index 36746f18a4..61672618df 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/SizeSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/SizeSelector.java @@ -2,18 +2,29 @@ import org.evomaster.client.java.controller.mongo.operations.*; +import java.util.Objects; + /** * { field: { $size: value } } */ public class SizeSelector extends SingleConditionQuerySelector { + + public static final String SIZE_OPERATOR = "$size"; + @Override protected QueryOperation parseValue(String fieldName, Object value) { - if (value instanceof Integer) return new SizeOperation(fieldName, (Integer) value); - return null; + Objects.requireNonNull(fieldName); + Objects.requireNonNull(value); + + if (value instanceof Integer) { + return new SizeOperation(fieldName, (Integer) value); + } else { + return null; + } } @Override protected String operator() { - return "$size"; + return SIZE_OPERATOR; } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/TypeSelector.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/TypeSelector.java index e2608b8322..77f39d97b8 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/TypeSelector.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/selectors/TypeSelector.java @@ -9,6 +9,9 @@ * { field: { $type: BSON type } } */ public class TypeSelector extends SingleConditionQuerySelector { + + public static final String TYPE_OPERATOR = "$type"; + @Override protected QueryOperation parseValue(String fieldName, Object value) { if (value instanceof Integer) return new TypeOperation(fieldName, getTypeFromNumber((Integer) value)); @@ -18,6 +21,6 @@ protected QueryOperation parseValue(String fieldName, Object value) { @Override protected String operator() { - return "$type"; + return TYPE_OPERATOR; } -} \ No newline at end of file +} diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java index 85ae9bb44c..57d1d50a1e 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/mongo/utils/BsonHelper.java @@ -2,72 +2,106 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Objects; import java.util.Set; public class BsonHelper { - public static Object newDocument(Object document) { + + private static final String APPEND_METHOD = "append"; + private static final String GET_METHOD = "get"; + private static final String KEY_SET_METHOD = "keySet"; + private static final String CONTAINS_KEY_METHOD = "containsKey"; + private static final String GET_TYPE_NAME_METHOD = "getTypeName"; + private static final String FIND_BY_VALUE_METHOD = "findByValue"; + private static final String VALUE_OF_METHOD = "valueOf"; + + private static final String ORG_BSON_BSON_TYPE = "org.bson.BsonType"; + private static final String ORG_BSON_DOCUMENT = "org.bson.Document"; + + public static Object newDocument(Object bsonDocument) { + Objects.requireNonNull(bsonDocument); + if (!isBsonDocument(bsonDocument)) { + throw new IllegalArgumentException("argument bsonDocument must be a BsonDocument"); + } try { - return document.getClass().getConstructor().newInstance(); + return bsonDocument.getClass().getConstructor().newInstance(); } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { throw new RuntimeException(e); } } - public static void appendToDocument(Object document, String fieldName, Object value) { + public static void appendToDocument(Object bsonDocument, String fieldName, Object value) { + Objects.requireNonNull(bsonDocument); + Objects.requireNonNull(fieldName); + if (!isBsonDocument(bsonDocument)) { + throw new IllegalArgumentException("argument bsonDocument must be a BsonDocument"); + } try { - Method append = document.getClass().getMethod("append", String.class, Object.class); - append.invoke(document, fieldName, value); + Method append = bsonDocument.getClass().getMethod(APPEND_METHOD, String.class, Object.class); + append.invoke(bsonDocument, fieldName, value); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(e); } } - public static Object getValue(Object document, String fieldName) { - try { - return document.getClass().getMethod("get", Object.class).invoke(document, fieldName); - } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - throw new RuntimeException(e); + public static Object getValue(Object bsonDocument, String fieldName) { + Objects.requireNonNull(bsonDocument); + Objects.requireNonNull(fieldName); + if (!isBsonDocument(bsonDocument)) { + throw new IllegalArgumentException("argument bsonDocument must be a BsonDocument"); } - } - - public static Set keySet(Object document) { try { - return (Set) document.getClass().getMethod("keySet").invoke(document); + return bsonDocument.getClass().getMethod(GET_METHOD, Object.class).invoke(bsonDocument, fieldName); } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { throw new RuntimeException(e); } } - public static Boolean documentContainsField(Object document, String field) { + public static Boolean documentContainsField(Object bsonDocument, String fieldName) { + Objects.requireNonNull(bsonDocument); + Objects.requireNonNull(fieldName); + if (!isBsonDocument(bsonDocument)) { + throw new IllegalArgumentException("argument bsonDocument must be a BsonDocument"); + } try { - return (Boolean) document.getClass().getMethod("containsKey", Object.class).invoke(document, field); + return (Boolean) bsonDocument.getClass().getMethod(CONTAINS_KEY_METHOD, Object.class).invoke(bsonDocument, fieldName); } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { throw new RuntimeException(e); } } - public static Set documentKeys(Object document) { + public static Set documentKeys(Object bsonDocument) { + Objects.requireNonNull(bsonDocument); + if (!isBsonDocument(bsonDocument)) { + throw new IllegalArgumentException("argument bsonDocument must be a BsonDocument"); + } try { - return (Set) document.getClass().getMethod("keySet").invoke(document); + return (Set) bsonDocument.getClass().getMethod(KEY_SET_METHOD).invoke(bsonDocument); } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - throw new RuntimeException(e); + return null; } } - public static Boolean isDocument(Object value) { - return value.getClass().getName().equals("org.bson.Document"); + /** + * Checks if the given object represents a BSON document. + * + * @param value the object to check; should be non-null to determine if it is a BSON document + * @return true if the object is a BSON document, false otherwise + */ + public static Boolean isBsonDocument(Object value) { + return value != null && value.getClass().getName().equals(ORG_BSON_DOCUMENT); } public static String getType(Object bsonType) { try { ClassLoader bsonTypeClassLoader = bsonType.getClass().getClassLoader(); Class bsonTypeClassMapClass = bsonTypeClassLoader.loadClass("org.bson.codecs.BsonTypeClassMap"); - Class bsonTypeClass = bsonTypeClassLoader.loadClass("org.bson.BsonType"); + Class bsonTypeClass = bsonTypeClassLoader.loadClass(ORG_BSON_BSON_TYPE); Object bsonTypeClassMap = bsonTypeClassMapClass.getDeclaredConstructor().newInstance(); - Method get = bsonTypeClassMapClass.getMethod("get", bsonTypeClass); + Method get = bsonTypeClassMapClass.getMethod(GET_METHOD, bsonTypeClass); Object type = get.invoke(bsonTypeClassMap, bsonType); - return (String) type.getClass().getMethod("getTypeName").invoke(type, null); + return (String) type.getClass().getMethod(GET_TYPE_NAME_METHOD).invoke(type, null); } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(e); @@ -77,8 +111,8 @@ public static String getType(Object bsonType) { public static Object getTypeFromNumber(Integer number) { Class bsonTypeClass; try { - bsonTypeClass = Class.forName("org.bson.BsonType"); - Method findByValue = bsonTypeClass.getMethod("findByValue", int.class); + bsonTypeClass = Class.forName(ORG_BSON_BSON_TYPE); + Method findByValue = bsonTypeClass.getMethod(FIND_BY_VALUE_METHOD, int.class); return findByValue.invoke(null, number); } catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { @@ -89,8 +123,8 @@ public static Object getTypeFromNumber(Integer number) { public static Object getTypeFromAlias(String alias) { Class bsonTypeClass; try { - bsonTypeClass = Class.forName("org.bson.BsonType"); - Method valueOf = bsonTypeClass.getMethod("valueOf", String.class); + bsonTypeClass = Class.forName(ORG_BSON_BSON_TYPE); + Method valueOf = bsonTypeClass.getMethod(VALUE_OF_METHOD, String.class); return valueOf.invoke(null, alias); } catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java new file mode 100644 index 0000000000..f598a5981c --- /dev/null +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/mongo/QueryParserTest.java @@ -0,0 +1,770 @@ +package org.evomaster.client.java.controller.mongo; + +import org.bson.Document; +import org.evomaster.client.java.controller.mongo.operations.*; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.*; + +class QueryParserTest { + + private final QueryParser parser = new QueryParser(); + + @Test + void testParseEquals() { + Document query = new Document( + "age", + new Document("$eq", 30) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof EqualsOperation); + EqualsOperation eq = (EqualsOperation) operation; + assertEquals("age", eq.getFieldName()); + assertEquals(30, eq.getValue()); + } + + @Test + void testParseEqualsNull() { + Document query = new Document( + "age", + new Document("$eq", null) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof EqualsOperation); + EqualsOperation eq = (EqualsOperation) operation; + assertEquals("age", eq.getFieldName()); + assertEquals(null, eq.getValue()); + } + + @Test + void testParseNotEquals() { + Document query = new Document( + "age", + new Document("$ne", 30) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof NotEqualsOperation); + NotEqualsOperation ne = (NotEqualsOperation) operation; + assertEquals("age", ne.getFieldName()); + assertEquals(30, ne.getValue()); + } + + @Test + void testParseNotEqualsNull() { + Document query = new Document( + "age", + new Document("$ne", null) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof NotEqualsOperation); + NotEqualsOperation ne = (NotEqualsOperation) operation; + assertEquals("age", ne.getFieldName()); + assertEquals(null, ne.getValue()); + } + + @Test + void testParseAnd() { + Document query = new Document( + "$and", + Arrays.asList( + new Document("age", new Document("$eq", 30)), + new Document("name", new Document("$eq", "John")))); + + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof AndOperation); + AndOperation and = (AndOperation) operation; + assertEquals(2, and.getConditions().size()); + } + + @Test + void testParseOr() { + Document query = new Document( + "$or", + Arrays.asList( + new Document("age", new Document("$eq", 30)), + new Document("name", new Document("$eq", "John")))); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof OrOperation); + OrOperation or = (OrOperation) operation; + assertEquals(2, or.getConditions().size()); + } + + @Test + void testParseIn() { + Document query = new Document( + "age", + new Document("$in", Arrays.asList(20, 30, 40)) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof InOperation); + InOperation in = (InOperation) operation; + assertEquals("age", in.getFieldName()); + assertEquals(Arrays.asList(20, 30, 40), in.getValues()); + } + + @Test + void testParseInWithNull() { + Document query = new Document( + "age", + new Document("$in", Arrays.asList(20, 30, null)) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof InOperation); + InOperation in = (InOperation) operation; + assertEquals("age", in.getFieldName()); + assertEquals(Arrays.asList(20, 30, null), in.getValues()); + } + + + + @Test + void testParseExists() { + Document query = new Document( + "age", + new Document("$exists", true) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof ExistsOperation); + ExistsOperation exists = (ExistsOperation) operation; + assertEquals("age", exists.getFieldName()); + assertTrue(exists.getBoolean()); + } + + @Test + void testParseNot() { + Document query = new Document( + "age", + new Document("$not", new Document("$eq", 30)) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof NotOperation); + NotOperation not = (NotOperation) operation; + assertEquals("age", not.getFieldName()); + assertTrue(not.getCondition() instanceof EqualsOperation); + } + + @Test + void testParseSize() { + Document query = new Document( + "tags", + new Document("$size", 3) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof SizeOperation); + SizeOperation size = (SizeOperation) operation; + assertEquals("tags", size.getFieldName()); + assertEquals(3, size.getValue()); + } + + @Test + void testParseAll() { + Document query = new Document( + "tags", + new Document("$all", Arrays.asList("a", "b")) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof AllOperation); + AllOperation all = (AllOperation) operation; + assertEquals("tags", all.getFieldName()); + assertEquals(Arrays.asList("a", "b"), all.getValues()); + } + + @Test + void testParseElemMatch() { + Document query = new Document( + "results", + new Document("$elemMatch", new Document("product", "abc")) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof ElemMatchOperation); + ElemMatchOperation elemMatch = (ElemMatchOperation) operation; + assertEquals("results", elemMatch.getFieldName()); + // Implicit equals inside elemMatch + assertTrue(elemMatch.getCondition() instanceof EqualsOperation); + EqualsOperation equals = (EqualsOperation) elemMatch.getCondition(); + assertEquals("product", equals.getFieldName()); + assertEquals("abc", equals.getValue()); + } + + @Test + void testParseElemMatchWithMultipleConditions() { + Document query = new Document( + "results", + new Document("$elemMatch", + new Document("product", "abc").append("quantity", new Document("$gt", 10))) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof ElemMatchOperation); + ElemMatchOperation elemMatch = (ElemMatchOperation) operation; + assertEquals("results", elemMatch.getFieldName()); + // Implicit and inside elemMatch + assertTrue(elemMatch.getCondition() instanceof AndOperation); + AndOperation and = (AndOperation) elemMatch.getCondition(); + assertEquals(2, and.getConditions().size()); + assertTrue(and.getConditions().get(0) instanceof EqualsOperation); + assertTrue(and.getConditions().get(1) instanceof GreaterThanOperation); + + } + + @Test + void testParseElemMatchNull() { + Document query = new Document( + "results", + new Document("$elemMatch", new Document("product", null)) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof ElemMatchOperation); + ElemMatchOperation elemMatch = (ElemMatchOperation) operation; + assertEquals("results", elemMatch.getFieldName()); + // Implicit equals inside elemMatch + assertTrue(elemMatch.getCondition() instanceof EqualsOperation); + EqualsOperation equals = (EqualsOperation) elemMatch.getCondition(); + assertEquals("product", equals.getFieldName()); + assertEquals(null, equals.getValue()); + } + + @Test + void testParseType() { + Document query = new Document( + "name", + new Document("$type", "STRING") + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof TypeOperation); + TypeOperation type = (TypeOperation) operation; + assertEquals("name", type.getFieldName()); + assertNotNull(type.getType()); + } + + @Test + void testParseTypeWithNumber() { + Document query = new Document( + "name", + new Document("$type", 2) // 2 is string + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof TypeOperation); + TypeOperation type = (TypeOperation) operation; + assertEquals("name", type.getFieldName()); + assertNotNull(type.getType()); + } + + @Test + void testParseGreaterThan() { + Document query = new Document( + "age", + new Document("$gt", 18) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof GreaterThanOperation); + GreaterThanOperation gt = (GreaterThanOperation) operation; + assertEquals("age", gt.getFieldName()); + assertEquals(18, gt.getValue()); + } + + @Test + void testParseLessThanEquals() { + Document query = new Document( + "age", + new Document("$lte", 65) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof LessThanEqualsOperation); + LessThanEqualsOperation lte = (LessThanEqualsOperation) operation; + assertEquals("age", lte.getFieldName()); + assertEquals(65, lte.getValue()); + } + + @Test + void testParseNor() { + Document query = new Document( + "$nor", + Arrays.asList( + new Document("a", 1), + new Document("b", 2))); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof NorOperation); + NorOperation nor = (NorOperation) operation; + assertEquals(2, nor.getConditions().size()); + } + + @Test + void testParseNotIn() { + Document query = new Document( + "age", + new Document("$nin", Arrays.asList(10, 20)) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof NotInOperation); + NotInOperation nin = (NotInOperation) operation; + assertEquals("age", nin.getFieldName()); + assertEquals(Arrays.asList(10, 20), nin.getValues()); + } + + @Test + void testParseMod() { + Document query = new Document( + "age", + new Document("$mod", Arrays.asList(2L, 0L)) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof ModOperation); + ModOperation mod = (ModOperation) operation; + assertEquals("age", mod.getFieldName()); + assertEquals(2L, mod.getDivisor()); + assertEquals(0L, mod.getRemainder()); + } + + @Test + void testParseGreaterThanEquals() { + Document query = new Document( + "age", + new Document("$gte", 18) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof GreaterThanEqualsOperation); + GreaterThanEqualsOperation gte = (GreaterThanEqualsOperation) operation; + assertEquals("age", gte.getFieldName()); + assertEquals(18, gte.getValue()); + } + + @Test + void testParseNearSphereLegacy() { + Document query = new Document( + "location", + new Document("$nearSphere", new Document("x", 40.0).append("y", 70.0)) + .append("$maxDistance", 10.0) + .append("$minDistance", 1.0) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof NearSphereOperation); + NearSphereOperation ns = (NearSphereOperation) operation; + assertEquals("location", ns.getFieldName()); + assertEquals(40.0, ns.getLongitude()); + assertEquals(70.0, ns.getLatitude()); + // Radians to meters: 6371000 * distance + assertEquals(6371000 * 10.0, ns.getMaxDistance()); + assertEquals(6371000 * 1.0, ns.getMinDistance()); + } + + @Test + void testParseNearSphereGeoJson() { + Document geometry = new Document("type", "Point") + .append("coordinates", Arrays.asList(40.0, 70.0)); + Document query = new Document( + "location", + new Document("$nearSphere", new Document("$geometry", geometry) + .append("$maxDistance", 1000.0) + .append("$minDistance", 100.0)) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof NearSphereOperation); + NearSphereOperation ns = (NearSphereOperation) operation; + assertEquals("location", ns.getFieldName()); + assertEquals(40.0, ns.getLongitude()); + assertEquals(70.0, ns.getLatitude()); + assertEquals(1000.0, ns.getMaxDistance()); + assertEquals(100.0, ns.getMinDistance()); + } + + @Test + void testParseImplicitEquals() { + Document query = new Document("age", 30); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof EqualsOperation); + EqualsOperation eq = (EqualsOperation) operation; + assertEquals("age", eq.getFieldName()); + assertEquals(30, eq.getValue()); + } + + @Test + void testParseImplicitEqualsNull() { + Document query = new Document("age", null); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof EqualsOperation); + EqualsOperation eq = (EqualsOperation) operation; + assertEquals("age", eq.getFieldName()); + assertEquals(null, eq.getValue()); + } + + @Test + void testParseImplicitAnd() { + Document query = new Document("age", 30).append("name", "John"); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof AndOperation); + AndOperation and = (AndOperation) operation; + assertEquals(2, and.getConditions().size()); + + assertTrue(and.getConditions().get(0) instanceof EqualsOperation); + EqualsOperation eq = (EqualsOperation) and.getConditions().get(0); + assertEquals("age", eq.getFieldName()); + assertEquals(30, eq.getValue()); + + assertTrue(and.getConditions().get(1) instanceof EqualsOperation); + EqualsOperation eq2 = (EqualsOperation) and.getConditions().get(1); + assertEquals("name", eq2.getFieldName()); + assertEquals("John", eq2.getValue()); + } + + @Test + void testParseImplicitAndEquals() { + Document query = new Document( + "age", + new Document("$gte", 18) + .append("$lt", 65) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof AndOperation); + AndOperation and = (AndOperation) operation; + assertEquals(2, and.getConditions().size()); + + assertTrue(and.getConditions().get(0) instanceof GreaterThanEqualsOperation); + GreaterThanEqualsOperation gte = (GreaterThanEqualsOperation) and.getConditions().get(0); + assertEquals("age", gte.getFieldName()); + assertEquals(18, gte.getValue()); + + assertTrue(and.getConditions().get(1) instanceof LessThanOperation); + LessThanOperation lt = (LessThanOperation) and.getConditions().get(1); + assertEquals("age", lt.getFieldName()); + assertEquals(65, lt.getValue()); + } + + + @Test + void testParseImplicitEqualsWithDocument() { + Document innerDoc = new Document("foo", "bar"); + Document query = new Document("metadata", innerDoc); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof EqualsOperation); + EqualsOperation eq = (EqualsOperation) operation; + assertEquals("metadata", eq.getFieldName()); + assertEquals(innerDoc, eq.getValue()); + } + + @Test + void testParseMultipleOperatorsOnSameField() { + Document query = new Document( + "age", + new Document("$gt", 18).append("$lt", 30) + ); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof AndOperation); + AndOperation and = (AndOperation) operation; + assertEquals(2, and.getConditions().size()); + + assertTrue(and.getConditions().get(0) instanceof GreaterThanOperation); + assertTrue(and.getConditions().get(1) instanceof LessThanOperation); + } + + @Test + void testParseComplexNestedQuery() { + Document query = new Document("$and", + Arrays.asList( + new Document("age", new Document("$gte", 18)), + new Document("$or", + Arrays.asList( + new Document("status", "active"), + new Document("verified", true))))); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof AndOperation); + AndOperation and = (AndOperation) operation; + assertEquals(2, and.getConditions().size()); + + assertTrue(and.getConditions().get(0) instanceof GreaterThanEqualsOperation); + assertTrue(and.getConditions().get(1) instanceof OrOperation); + + OrOperation or = (OrOperation) and.getConditions().get(1); + assertEquals(2, or.getConditions().size()); + } + + @Test + void testParseImplicitEqualsWithMultipleFields() { + Document query = new Document("age", 30).append("city", "London"); + QueryOperation operation = parser.parse(query); + assertTrue(operation instanceof AndOperation); + AndOperation and = (AndOperation) operation; + assertEquals(2, and.getConditions().size()); + + // Assert that both are EqualsOperations + assertTrue(and.getConditions().stream().allMatch(c -> c instanceof EqualsOperation)); + } + + @Test + void testParseImplicitEqualsWithEmptyDocument() { + Document query = new Document(); + QueryOperation operation = parser.parse(query); + assertNull(operation); + } + + @Test + void testParseInvalidUnknownOperator() { + Document query = new Document("age", new Document("$unknown", 30)); + QueryOperation operation = parser.parse(query); + assertNull(operation); + } + + @Test + void testParseInvalidTopLevelNotQuery() { + Document query = new Document("$not", 30); + + QueryOperation operation = parser.parse(query); + + assertNull(operation); + } + + @Test + void testParseInvalidTopLevelNotWithDocument() { + Document query = new Document( + "$not", + new Document("age", new Document("$eq", 30)) + ); + + QueryOperation operation = parser.parse(query); + + assertNull(operation); + } + + @Test + void testParseInvalidNotWithLiteralValue() { + Document query = new Document( + "country", + new Document("$not", "USA") + ); + + QueryOperation operation = parser.parse(query); + + assertNull(operation); + } + + @Test + void testParseInvalidNotWithNumericLiteral() { + Document query = new Document( + "age", + new Document("$not", 30) + ); + + QueryOperation operation = parser.parse(query); + + assertNull(operation); + } + + @Test + void testParseInvalidNotWithNullValue() { + Document query = new Document( + "age", + new Document("$not", null) + ); + + QueryOperation operation = parser.parse(query); + + assertNull(operation); + } + + @Test + void testParseInvalidNotWithAndOperator() { + Document query = new Document( + "age", + new Document( + "$not", + new Document( + "$and", + Arrays.asList( + new Document("age", new Document("$lt", 18)), + new Document("country", "USA"))) + ) + ); + + QueryOperation operation = parser.parse(query); + + assertNull(operation); + } + + @Test + void testParseInvalidNotWithOrOperator() { + Document query = new Document( + "age", + new Document( + "$not", + new Document( + "$or", + new Document("age", new Document("$lt", 18)) + .append("country", "USA")))); + QueryOperation operation = parser.parse(query); + + assertNull(operation); + } + + @Test + void testParseInvalidUnknownTopLevelOperator() { + Document query = new Document( + "$unknown", + new Document("age", 30) + ); + + QueryOperation operation = parser.parse(query); + + assertNull(operation); + } + + @Test + void testParseInvalidUnknownFieldOperator() { + Document query = new Document( + "age", + new Document("$unknown", 30) + ); + + QueryOperation operation = parser.parse(query); + + assertNull(operation); + } + + @Test + void testParseInvalidAndWithNonArrayValue() { + Document query = new Document( + "$and", + new Document("age", 30) + ); + + QueryOperation operation = parser.parse(query); + + assertNull(operation); + } + + @Test + void testParseInvalidOrWithNonArrayValue() { + Document query = new Document("$or", 30); + + QueryOperation operation = parser.parse(query); + + assertNull(operation); + } + + @Test + void testParseInvalidNorWithNonArrayValue() { + Document query = new Document("$nor", "invalid"); + + QueryOperation operation = parser.parse(query); + + assertNull(operation); + } + + @Test + void testParseInvalidAndWithLiteralElement() { + Document query = new Document( + "$and", + Arrays.asList( + new Document("age", 30), + "invalid" + ) + ); + + QueryOperation operation = parser.parse(query); + + assertNull(operation); + } + + @Test + void testParseInvalidOrWithNumericElement() { + Document query = new Document( + "$or", + Arrays.asList( + new Document("age", 30), + 42 + ) + ); + + QueryOperation operation = parser.parse(query); + + assertNull(operation); + } + + @Test + void testParseInvalidEmptyOperatorDocument() { + Document query = new Document( + "age", + new Document() + ); + + QueryOperation operation = parser.parse(query); + + assertNull(operation); + } + + @Test + void testParseInvalidComparisonOperatorWithMissingValue() { + Document query = new Document( + "age", + new Document("$lt", null) + ); + + QueryOperation operation = parser.parse(query); + + assertNull(operation); + } + + @Test + void testParseInvalidLessThanNull() { + Document query = new Document( + "age", + new Document("$lt", null) + ); + QueryOperation operation = parser.parse(query); + assertNull(operation); + } + + @Test + void testParseInvalidLessEqualsThanNull() { + Document query = new Document( + "age", + new Document("$lte", null) + ); + QueryOperation operation = parser.parse(query); + assertNull(operation); + } + + @Test + void testParseGreaterThenEqualsThanNull() { + Document query = new Document( + "age", + new Document("$gte", null) + ); + QueryOperation operation = parser.parse(query); + assertNull(operation); + } + + @Test + void testParseGreaterThenThanNull() { + Document query = new Document( + "age", + new Document("$gt", null) + ); + QueryOperation operation = parser.parse(query); + assertNull(operation); + } + + + @Test + void testParseInvalidMultipleOperatorsIncludingUnknownOperator() { + Document query = new Document( + "age", + new Document("$gte", 18) + .append("$unknown", 30) + ); + + QueryOperation operation = parser.parse(query); + + assertNull(operation); + } + + @Test + void testParseInvalidExistsNull() { + Document query = new Document( + "age", + new Document("$exists", null) + ); + QueryOperation operation = parser.parse(query); + assertNull(operation); + } +}