Skip to content

Commit d6d8a13

Browse files
committed
DATAMONGO-423 - Fixed handling of negated regular expressions.
When using the not() method combined with the regex(…) methods on Criteria we created an invalid query so far. Fixed the regex(…) method to always transform the regex expressions and options into a Pattern instance and render that according to the $not state.
1 parent 3dcf937 commit d6d8a13

File tree

3 files changed

+66
-14
lines changed

3 files changed

+66
-14
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java

+39-11
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@
2020
import java.util.Collection;
2121
import java.util.LinkedHashMap;
2222
import java.util.List;
23+
import java.util.regex.Pattern;
2324

25+
import org.bson.BSON;
2426
import org.bson.types.BasicBSONList;
2527
import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
2628
import org.springframework.data.mongodb.core.geo.Circle;
2729
import org.springframework.data.mongodb.core.geo.Point;
2830
import org.springframework.data.mongodb.core.geo.Shape;
2931
import org.springframework.util.Assert;
30-
import org.springframework.util.StringUtils;
3132

3233
import com.mongodb.BasicDBObject;
3334
import com.mongodb.DBObject;
@@ -97,13 +98,17 @@ public Criteria is(Object o) {
9798
throw new InvalidMongoDbApiUsageException(
9899
"Multiple 'is' values declared. You need to use 'and' with multiple criteria");
99100
}
100-
if (this.criteria.size() > 0 && "$not".equals(this.criteria.keySet().toArray()[this.criteria.size() - 1])) {
101+
if (lastOperatorWasNot()) {
101102
throw new InvalidMongoDbApiUsageException("Invalid query: 'not' can't be used with 'is' - use 'ne' instead.");
102103
}
103104
this.isValue = o;
104105
return this;
105106
}
106107

108+
private boolean lastOperatorWasNot() {
109+
return this.criteria.size() > 0 && "$not".equals(this.criteria.keySet().toArray()[this.criteria.size() - 1]);
110+
}
111+
107112
/**
108113
* Creates a criterion using the $ne operator
109114
*
@@ -269,7 +274,11 @@ public Criteria type(int t) {
269274
* @return
270275
*/
271276
public Criteria not() {
272-
criteria.put("$not", null);
277+
return not(null);
278+
}
279+
280+
private Criteria not(Object value) {
281+
criteria.put("$not", value);
273282
return this;
274283
}
275284

@@ -280,8 +289,7 @@ public Criteria not() {
280289
* @return
281290
*/
282291
public Criteria regex(String re) {
283-
criteria.put("$regex", re);
284-
return this;
292+
return regex(re, null);
285293
}
286294

287295
/**
@@ -292,13 +300,32 @@ public Criteria regex(String re) {
292300
* @return
293301
*/
294302
public Criteria regex(String re, String options) {
295-
criteria.put("$regex", re);
296-
if (StringUtils.hasText(options)) {
297-
criteria.put("$options", options);
303+
return regex(toPattern(re, options));
304+
}
305+
306+
/**
307+
* Syntactical sugar for {@link #is(Object)} making obvious that we create a regex predicate.
308+
*
309+
* @param pattern
310+
* @return
311+
*/
312+
public Criteria regex(Pattern pattern) {
313+
314+
Assert.notNull(pattern);
315+
316+
if (lastOperatorWasNot()) {
317+
return not(pattern);
298318
}
319+
320+
this.isValue = pattern;
299321
return this;
300322
}
301323

324+
private Pattern toPattern(String regex, String options) {
325+
Assert.notNull(regex);
326+
return Pattern.compile(regex, options == null ? 0 : BSON.regexFlags(options));
327+
}
328+
302329
/**
303330
* Creates a geospatial criterion using a $within $center operation. This is only available for Mongo 1.7 and higher.
304331
*
@@ -426,16 +453,17 @@ protected DBObject getSingleCriteriaObject() {
426453
DBObject dbo = new BasicDBObject();
427454
boolean not = false;
428455
for (String k : this.criteria.keySet()) {
456+
Object value = this.criteria.get(k);
429457
if (not) {
430458
DBObject notDbo = new BasicDBObject();
431-
notDbo.put(k, this.criteria.get(k));
459+
notDbo.put(k, value);
432460
dbo.put("$not", notDbo);
433461
not = false;
434462
} else {
435-
if ("$not".equals(k)) {
463+
if ("$not".equals(k) && value == null) {
436464
not = true;
437465
} else {
438-
dbo.put(k, this.criteria.get(k));
466+
dbo.put(k, value);
439467
}
440468
}
441469
}

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java

+25-1
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@
4646
import org.springframework.data.mongodb.core.convert.CustomConversions;
4747
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
4848
import org.springframework.data.mongodb.core.index.Index;
49+
import org.springframework.data.mongodb.core.index.Index.Duplicates;
4950
import org.springframework.data.mongodb.core.index.IndexField;
5051
import org.springframework.data.mongodb.core.index.IndexInfo;
51-
import org.springframework.data.mongodb.core.index.Index.Duplicates;
5252
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
5353
import org.springframework.data.mongodb.core.query.Criteria;
5454
import org.springframework.data.mongodb.core.query.Order;
@@ -131,6 +131,7 @@ protected void cleanDb() {
131131
template.dropCollection(template.getCollectionName(PersonWithIdPropertyOfTypeLong.class));
132132
template.dropCollection(template.getCollectionName(PersonWithIdPropertyOfPrimitiveLong.class));
133133
template.dropCollection(template.getCollectionName(TestClass.class));
134+
template.dropCollection(Sample.class);
134135
}
135136

136137
@Test
@@ -1084,10 +1085,33 @@ public void removesEntityWithAnnotatedIdIfIdNeedsMassaging() {
10841085
assertThat(template.findOne(query(where("id").is(id)), Sample.class), is(nullValue()));
10851086
}
10861087

1088+
/**
1089+
* @see DATAMONGO-423
1090+
*/
1091+
@Test
1092+
public void executesQueryWithNegatedRegexCorrectly() {
1093+
1094+
Sample first = new Sample();
1095+
first.field = "Matthews";
1096+
1097+
Sample second = new Sample();
1098+
second.field = "Beauford";
1099+
1100+
template.save(first);
1101+
template.save(second);
1102+
1103+
Query query = query(where("field").not().regex("Matthews"));
1104+
System.out.println(query.getQueryObject());
1105+
List<Sample> result = template.find(query, Sample.class);
1106+
assertThat(result.size(), is(1));
1107+
assertThat(result.get(0).field, is("Beauford"));
1108+
}
1109+
10871110
public static class Sample {
10881111

10891112
@Id
10901113
String id;
1114+
String field;
10911115
}
10921116

10931117
static class TestClass {

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/QueryTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public void testSimpleQueryWithChainedCriteria() {
100100
public void testComplexQueryWithMultipleChainedCriteria() {
101101
Query q = new Query(where("name").regex("^T.*").and("age").gt(20).lt(80).and("city")
102102
.in("Stockholm", "London", "New York"));
103-
String expected = "{ \"name\" : { \"$regex\" : \"^T.*\"} , \"age\" : { \"$gt\" : 20 , \"$lt\" : 80} , "
103+
String expected = "{ \"name\" : { \"$regex\" : \"^T.*\" , \"$options\" : \"\"} , \"age\" : { \"$gt\" : 20 , \"$lt\" : 80} , "
104104
+ "\"city\" : { \"$in\" : [ \"Stockholm\" , \"London\" , \"New York\"]}}";
105105
Assert.assertEquals(expected, q.getQueryObject().toString());
106106
}
@@ -134,7 +134,7 @@ public void testQueryWithIn() {
134134
@Test
135135
public void testQueryWithRegex() {
136136
Query q = new Query(where("name").regex("b.*"));
137-
String expected = "{ \"name\" : { \"$regex\" : \"b.*\"}}";
137+
String expected = "{ \"name\" : { \"$regex\" : \"b.*\" , \"$options\" : \"\"}}";
138138
Assert.assertEquals(expected, q.getQueryObject().toString());
139139
}
140140

0 commit comments

Comments
 (0)