diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbDocumentSerializer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbDocumentSerializer.java index 216092127..1b3c726f0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbDocumentSerializer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbDocumentSerializer.java @@ -21,6 +21,8 @@ import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Queue; +import java.util.Set; import java.util.regex.Pattern; import org.bson.BsonJavaScript; @@ -183,25 +185,16 @@ abstract class MongodbDocumentSerializer implements Visitor { return asDocument(asDBKey(expr, 0), ""); } else if (op == Ops.AND) { - List> pendingDocuments = new LinkedList<>(); - for (int i = 0; i < 2; i++) { - Map document = (Map) handle(expr.getArg(i)); - if (document.keySet().size() == 1 && document.containsKey("$and")) { - pendingDocuments.addAll((Collection>) document.get("$and")); - } else { - pendingDocuments.add(document); - } - } + Queue> pendingDocuments = collectConnectorArgs("$and", expr); + List> unmergeableDocuments = new ArrayList<>(); + List> generatedDocuments = new ArrayList<>(); - List> unmergeableDocuments = new LinkedList<>(); + while (!pendingDocuments.isEmpty()) { - List> generatedDocuments = new LinkedList<>(); - - do { - Map lhs = pendingDocuments.remove(0); + Map lhs = pendingDocuments.poll(); for (Map rhs : pendingDocuments) { - LinkedHashSet lhs2 = new LinkedHashSet<>(lhs.keySet()); + Set lhs2 = new LinkedHashSet<>(lhs.keySet()); lhs2.retainAll(rhs.keySet()); if (lhs2.isEmpty()) { lhs.putAll(rhs); @@ -211,18 +204,11 @@ abstract class MongodbDocumentSerializer implements Visitor { } generatedDocuments.add(lhs); - pendingDocuments = unmergeableDocuments; + pendingDocuments = new LinkedList<>(unmergeableDocuments); unmergeableDocuments = new LinkedList<>(); - } while(!pendingDocuments.isEmpty()); - - if (generatedDocuments.size() == 1) { - return generatedDocuments.get(0); - } else { - List list = new ArrayList<>(expr.getArgs().size()); - list.addAll(generatedDocuments); - return asDocument("$and", list); } + return generatedDocuments.size() == 1 ? generatedDocuments.get(0) : asDocument("$and", generatedDocuments); } else if (op == Ops.NOT) { // Handle the not's child Operation subOperation = (Operation) expr.getArg(0); @@ -237,18 +223,7 @@ abstract class MongodbDocumentSerializer implements Visitor { } } else if (op == Ops.OR) { - - List list = new LinkedList<>(); - for (int i = 0; i < 2; i++) { - Map document = (Map) handle(expr.getArg(i)); - if (document.keySet().size() == 1 && document.containsKey("$or")) { - list.addAll((Collection) document.get("$or")); - } else { - list.add(document); - } - } - return asDocument("$or", list); - + return asDocument("$or", collectConnectorArgs("$or", expr)); } else if (op == Ops.NE) { Path path = (Path) expr.getArg(0); @@ -466,4 +441,19 @@ abstract class MongodbDocumentSerializer implements Visitor { public Object visit(ParamExpression expr, Void context) { throw new UnsupportedOperationException(); } + + private LinkedList> collectConnectorArgs(String operator, Operation operation) { + + LinkedList> pendingDocuments = new LinkedList<>(); + for (Expression exp : operation.getArgs()) { + Map document = (Map) handle(exp); + if (document.keySet().size() == 1 && document.containsKey(operator)) { + pendingDocuments.addAll((Collection>) document.get(operator)); + } else { + pendingDocuments.add(document); + } + } + return pendingDocuments; + + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializerUnitTests.java index 7ad95cf67..f6a7dc412 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializerUnitTests.java @@ -20,6 +20,7 @@ import static com.querydsl.core.types.ExpressionUtils.predicate; import static com.querydsl.core.types.dsl.Expressions.*; import static org.assertj.core.api.Assertions.*; +import java.util.Arrays; import java.util.Collections; import org.bson.Document; @@ -29,7 +30,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; - import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.WritingConverter; import org.springframework.data.mongodb.core.convert.DbRefResolver; @@ -179,25 +179,52 @@ public class SpringDataMongodbSerializerUnitTests { @Test // DATAMONGO-2475 public void chainedOrsInSameDocument() { - Predicate predicate = QPerson.person.firstname.eq("firstname_value").or( - QPerson.person.lastname.eq("lastname_value")).or(QPerson.person.age.goe(30)).or( - QPerson.person.age.loe(20)).or(QPerson.person.uniqueId.isNull()); + Predicate predicate = QPerson.person.firstname.eq("firstname_value") + .or(QPerson.person.lastname.eq("lastname_value")).or(QPerson.person.age.goe(30)).or(QPerson.person.age.loe(20)) + .or(QPerson.person.uniqueId.isNull()); assertThat(serializer.handle(predicate)).isEqualTo(Document.parse( "{\"$or\": [{\"firstname\": \"firstname_value\"}, {\"lastname\": \"lastname_value\"}, {\"age\": {\"$gte\": 30}}, {\"age\": {\"$lte\": 20}}, {\"uniqueId\": {\"$exists\": false}}]}")); } + @Test // DATAMONGO-2475 + public void chainedNestedOrsInSameDocument() { + + Predicate predicate = QPerson.person.firstname.eq("firstname_value") + .or(QPerson.person.lastname.eq("lastname_value")).or(QPerson.person.address.street.eq("spring")); + + assertThat(serializer.handle(predicate)).isEqualTo(Document.parse( + "{\"$or\": [{\"firstname\": \"firstname_value\"}, {\"lastname\": \"lastname_value\"}, {\"add.street\": \"spring\"}]}")); + } + @Test // DATAMONGO-2475 public void chainedAndsInSameDocument() { - Predicate predicate = QPerson.person.firstname.eq("firstname_value").and( - QPerson.person.lastname.eq("lastname_value")).and(QPerson.person.age.goe(30)).and( - QPerson.person.age.loe(20)).and(QPerson.person.uniqueId.isNull()); + Predicate predicate = QPerson.person.firstname.eq("firstname_value") + .and(QPerson.person.lastname.eq("lastname_value")).and(QPerson.person.age.goe(30)) + .and(QPerson.person.age.loe(20)).and(QPerson.person.uniqueId.isNull()); assertThat(serializer.handle(predicate)).isEqualTo(Document.parse( "{\"$and\": [{\"firstname\": \"firstname_value\", \"lastname\": \"lastname_value\", \"age\": {\"$gte\": 30}, \"uniqueId\": {\"$exists\": false}}, {\"age\": {\"$lte\": 20}}]}")); } + @Test // DATAMONGO-2475 + void chainMultipleAndFlattensCorrectly() { + + Document p1doc = Document.parse("{ \"$or\" : [ { \"firstname\" : \"fn\"}, { \"lastname\" : \"ln\" } ] }"); + Document p2doc = Document + .parse("{ \"$or\" : [ { \"age\" : { \"$gte\" : 20 } }, { \"age\" : { \"$lte\" : 30} } ] }"); + Document p3doc = Document.parse("{ \"$or\" : [ { \"add.city\" : \"c\"}, { \"add.zipCode\" : \"0\" } ] }"); + Document expected = new Document("$and", Arrays.asList(p1doc, p2doc, p3doc)); + + Predicate predicate1 = QPerson.person.firstname.eq("fn").or(QPerson.person.lastname.eq("ln")); + Predicate predicate2 = QPerson.person.age.goe(20).or(QPerson.person.age.loe(30)); + Predicate predicate3 = QPerson.person.address.city.eq("c").or(QPerson.person.address.zipCode.eq("0")); + PredicateOperation testExpression = predicate(Ops.AND, predicate1, predicate2, predicate3); + + assertThat(serializer.handle(testExpression)).isEqualTo(expected); + } + class Address { String id; String street;