diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java index 99dcb21e1..b84fc529b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map.Entry; +import java.util.function.Consumer; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -394,7 +395,41 @@ public class Criteria implements CriteriaDefinition { * @see MongoDB Query operator: $not */ public Criteria not() { - return not(null); + return not((Object)null); + } + + public Criteria not(Consumer source) { + + Criteria sink = new Criteria(this.key); + source.accept(sink); + + List target = new ArrayList<>(); + for(Criteria criteria : sink.criteriaChain) { + boolean nextIsNot = false; + for(Entry entry : criteria.criteria.entrySet()) { + if(entry.getKey().equals("$not")) { + nextIsNot = true; + continue; + } + + Criteria extractedSingleCriteria = new Criteria(criteria.key); + if(nextIsNot) { + extractedSingleCriteria = new Criteria("$not"); + extractedSingleCriteria.criteria.put(criteria.key, new Document(entry.getKey(), entry.getValue())); + nextIsNot = false; + } else { + extractedSingleCriteria.criteria.put(entry.getKey(), entry.getValue()); + } + target.add(extractedSingleCriteria); + } + } + List bsonList = createCriteriaList(target.toArray(new Criteria[0]), true); + return registerCriteriaChainElement(new Criteria("$and").is(bsonList)); + } + + public Criteria not(Criteria... criteria) { + List bsonList = createCriteriaList(criteria, true); + return registerCriteriaChainElement(new Criteria("$and").is(bsonList)); } /** @@ -660,7 +695,7 @@ public class Criteria implements CriteriaDefinition { * @param criteria */ public Criteria orOperator(Criteria... criteria) { - BasicDBList bsonList = createCriteriaList(criteria); + List bsonList = createCriteriaList(criteria, false); return registerCriteriaChainElement(new Criteria("$or").is(bsonList)); } @@ -674,7 +709,7 @@ public class Criteria implements CriteriaDefinition { * @param criteria */ public Criteria norOperator(Criteria... criteria) { - BasicDBList bsonList = createCriteriaList(criteria); + List bsonList = createCriteriaList(criteria, false); return registerCriteriaChainElement(new Criteria("$nor").is(bsonList)); } @@ -688,7 +723,7 @@ public class Criteria implements CriteriaDefinition { * @param criteria */ public Criteria andOperator(Criteria... criteria) { - BasicDBList bsonList = createCriteriaList(criteria); + List bsonList = createCriteriaList(criteria, false); return registerCriteriaChainElement(new Criteria("$and").is(bsonList)); } @@ -775,16 +810,23 @@ public class Criteria implements CriteriaDefinition { queryCriteria.put(this.key, this.isValue); queryCriteria.putAll(document); } else { - queryCriteria.put(this.key, document); + if(!document.isEmpty()) { + queryCriteria.put(this.key, document); + } } return queryCriteria; } - private BasicDBList createCriteriaList(Criteria[] criteria) { - BasicDBList bsonList = new BasicDBList(); + private List createCriteriaList(Criteria[] criteria, boolean not) { + List bsonList = new ArrayList(); for (Criteria c : criteria) { - bsonList.add(c.getCriteriaObject()); + + Document co = c.getCriteriaObject(); + if(not) { + co = new Document("$not", co); + } + bsonList.add(co); } return bsonList; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/QueryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/QueryTests.java index 6e4b92086..9976c415a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/QueryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/QueryTests.java @@ -19,6 +19,8 @@ import static org.assertj.core.api.Assertions.*; import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.Query.*; +import java.util.function.Consumer; + import org.bson.Document; import org.junit.jupiter.api.Test; import org.springframework.aop.framework.ProxyFactory; @@ -350,6 +352,44 @@ class QueryTests { compareQueries(target, source); } + + @Test // DATAMONGO-2499 + void notCombiningSeveralIndedependentCriterias() { + + Query source = new Query(new Criteria().not(Criteria.where("age").gt(30), Criteria.where("age").lt(20))); + Document target = source.getQueryObject(); + + assertThat(target).isEqualTo(Document.parse("{\"$and\": [{\"$not\": {\"age\": {\"$gt\": 30}}}, {\"$not\": {\"age\": {\"$lt\": 20}}}]}")); + } + + @Test // DATAMONGO-2499 + void notCombiningSingleCriteria() { + + Query source = new Query(Criteria.where("age").not(age -> age.gt(30).lt(20))); + Document target = source.getQueryObject(); + + assertThat(target).isEqualTo(Document.parse("{\"$and\": [{\"$not\": {\"age\": {\"$gt\": 30}}}, {\"$not\": {\"age\": {\"$lt\": 20}}}]}")); + } + + @Test // DATAMONGO-2499 + void notCombiningSingleCriteriaWithAnother() { + + Query source = new Query(Criteria.where("age").not(age -> age.gt(30).lt(20)).exists(true)); + Document target = source.getQueryObject(); + + assertThat(target).isEqualTo(Document.parse("{\"$and\": [{\"$not\": {\"age\": {\"$gt\": 30}}}, {\"$not\": {\"age\": {\"$lt\": 20}}}], \"age\": {\"$exists\": true}}")); + } + + @Test // DATAMONGO-2499 + void notCombiningSingleCriteriaWithNestedNot() { + + Query source = new Query(Criteria.where("age").not(age -> age.gt(30).not().lt(20))); + Document target = source.getQueryObject(); + + assertThat(target).isEqualTo(Document.parse("{\"$and\": [{\"$not\": {\"age\": {\"$gt\": 30}}}, {\"$not\": { \"$not\": {\"age\": {\"$lt\": 20}}}}]}")); + } + + private void compareQueries(Query actual, Query expected) { assertThat(actual.getCollation()).isEqualTo(expected.getCollation());