Skip to content

Commit 76df4ae

Browse files
authored
partial string match (#839)
* feat: Search $all strings starting with given strings * fix: Use regex for containsAllStartsWith query condition * doc: Update containsAll documentation * refactor: Use KeyConstraints in containsAllStartsWith Reverts ParseEncoder changes * refactor: Revert line change * fix: containsAllStartingWith for offline datastore * fix: Constraints for $all $regex are KeyConstraints and not strings * fix: Remove unexistent call * Revert stuff
1 parent eea6dd4 commit 76df4ae

File tree

4 files changed

+270
-13
lines changed

4 files changed

+270
-13
lines changed

Parse/src/main/java/com/parse/OfflineQueryLogic.java

Lines changed: 98 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -237,12 +237,24 @@ private static boolean matchesEqualConstraint(Object constraint, Object value) {
237237
return lhs.equals(rhs);
238238
}
239239

240-
return compare(constraint, value, new Decider() {
241-
@Override
242-
public boolean decide(Object constraint, Object value) {
243-
return constraint.equals(value);
244-
}
245-
});
240+
Decider decider;
241+
if (isStartsWithRegex(constraint)) {
242+
decider = new Decider() {
243+
@Override
244+
public boolean decide(Object constraint, Object value) {
245+
return ((String) value).matches(((KeyConstraints)constraint).get("$regex").toString());
246+
}
247+
};
248+
} else {
249+
decider = new Decider() {
250+
@Override
251+
public boolean decide(Object constraint, Object value) {
252+
return constraint.equals(value);
253+
}
254+
};
255+
}
256+
257+
return compare(constraint, value, decider);
246258
}
247259

248260
/**
@@ -348,6 +360,13 @@ private static boolean matchesAllConstraint(Object constraint, Object value) {
348360
}
349361

350362
if (constraint instanceof Collection) {
363+
if (isAnyValueRegexStartsWith((Collection<?>) constraint)) {
364+
constraint = cleanRegexStartsWith((Collection<?>) constraint);
365+
if (constraint == null) {
366+
throw new IllegalArgumentException("All values in $all queries must be of starting with regex or non regex.");
367+
}
368+
}
369+
351370
for (Object requiredItem : (Collection<?>) constraint) {
352371
if (!matchesEqualConstraint(requiredItem, value)) {
353372
return false;
@@ -358,6 +377,79 @@ private static boolean matchesAllConstraint(Object constraint, Object value) {
358377
throw new IllegalArgumentException("Constraint type not supported for $all queries.");
359378
}
360379

380+
/**
381+
* Check if any of the collection constraints is a regex to match strings that starts with another
382+
* string.
383+
*/
384+
private static boolean isAnyValueRegexStartsWith(Collection<?> constraints) {
385+
for (Object constraint : constraints) {
386+
if (isStartsWithRegex(constraint)) {
387+
return true;
388+
}
389+
};
390+
391+
return false;
392+
}
393+
394+
/**
395+
* Cleans all regex constraints. If any of the constraints is not a regex, then null is returned.
396+
* All values in a $all constraint must be a starting with another string regex.
397+
*/
398+
private static Collection<?> cleanRegexStartsWith(Collection<?> constraints) {
399+
ArrayList<KeyConstraints> cleanedValues = new ArrayList<>();
400+
for (Object constraint : constraints) {
401+
if (!(constraint instanceof KeyConstraints)) {
402+
return null;
403+
}
404+
405+
KeyConstraints cleanedRegex = cleanRegexStartsWith((KeyConstraints) constraint);
406+
if (cleanedRegex == null) {
407+
return null;
408+
}
409+
410+
cleanedValues.add(cleanedRegex);
411+
}
412+
413+
return cleanedValues;
414+
}
415+
416+
/**
417+
* Creates a regex pattern to match a substring at the beginning of another string.
418+
*
419+
* If given string is not a regex to match a string at the beginning of another string, then null
420+
* is returned.
421+
*/
422+
private static KeyConstraints cleanRegexStartsWith(KeyConstraints regex) {
423+
if (!isStartsWithRegex(regex)) {
424+
return null;
425+
}
426+
427+
// remove all instances of \Q and \E from the remaining text & escape single quotes
428+
String literalizedString = ((String)regex.get("$regex"))
429+
.replaceAll("([^\\\\])(\\\\E)", "$1")
430+
.replaceAll("([^\\\\])(\\\\Q)", "$1")
431+
.replaceAll("^\\\\E", "")
432+
.replaceAll("^\\\\Q", "")
433+
.replaceAll("([^'])'", "$1''")
434+
.replaceAll("^'([^'])", "''$1");
435+
436+
regex.put("$regex", literalizedString + ".*");
437+
return regex;
438+
}
439+
440+
/**
441+
* Check if given constraint is a regex to match strings that starts with another string.
442+
*/
443+
private static boolean isStartsWithRegex(Object constraint) {
444+
if (constraint == null || !(constraint instanceof KeyConstraints)) {
445+
return false;
446+
}
447+
448+
KeyConstraints keyConstraints = (KeyConstraints) constraint;
449+
return keyConstraints.size() == 1 && keyConstraints.containsKey("$regex") &&
450+
((String)keyConstraints.get("$regex")).startsWith("^");
451+
}
452+
361453
/**
362454
* Matches $regex constraints.
363455
*/

Parse/src/main/java/com/parse/ParseQuery.java

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
*/
99
package com.parse;
1010

11+
import android.support.annotation.NonNull;
12+
1113
import org.json.JSONException;
1214
import org.json.JSONObject;
1315

@@ -1673,10 +1675,6 @@ public ParseQuery<T> whereContainedIn(String key, Collection<? extends Object> v
16731675
}
16741676

16751677
/**
1676-
* Add a constraint to the query that requires a particular key's value match another
1677-
* {@code ParseQuery}.
1678-
* <p/>
1679-
* This only works on keys whose values are {@link ParseObject}s or lists of {@link ParseObject}s.
16801678
* Add a constraint to the query that requires a particular key's value to contain every one of
16811679
* the provided list of values.
16821680
*
@@ -1708,6 +1706,27 @@ public ParseQuery<T> whereFullText(String key, String text) {
17081706
return this;
17091707
}
17101708

1709+
/**
1710+
* Add a constraint to the query that requires a particular key's value to contain each one of
1711+
* the provided list of strings entirely or just starting with given strings.
1712+
*
1713+
* @param key
1714+
* The key to check. This key's value must be an array.
1715+
* @param values
1716+
* The values that will match entirely or starting with them.
1717+
* @return this, so you can chain this call.
1718+
*/
1719+
public ParseQuery<T> whereContainsAllStartsWith(String key, Collection<String> values) {
1720+
ArrayList<KeyConstraints> startsWithConstraints = new ArrayList<>();
1721+
for (String value : values) {
1722+
KeyConstraints keyConstraints = new KeyConstraints();
1723+
keyConstraints.put("$regex", buildStartsWithRegex(value));
1724+
startsWithConstraints.add(keyConstraints);
1725+
}
1726+
1727+
return whereContainsAll(key, startsWithConstraints);
1728+
}
1729+
17111730
/**
17121731
* Add a constraint to the query that requires a particular key's value match another
17131732
* {@code ParseQuery}.
@@ -1988,7 +2007,7 @@ public ParseQuery<T> whereContains(String key, String substring) {
19882007
* @return this, so you can chain this call.
19892008
*/
19902009
public ParseQuery<T> whereStartsWith(String key, String prefix) {
1991-
String regex = "^" + Pattern.quote(prefix);
2010+
String regex = buildStartsWithRegex(prefix);
19922011
whereMatches(key, regex);
19932012
return this;
19942013
}
@@ -2192,4 +2211,15 @@ public ParseQuery<T> setTrace(boolean shouldTrace) {
21922211
builder.setTracingEnabled(shouldTrace);
21932212
return this;
21942213
}
2214+
2215+
/**
2216+
* Helper method to convert a string to regex for start word matching.
2217+
*
2218+
* @param prefix String to use as prefix in regex.
2219+
* @return The string converted as regex for start word matching.
2220+
*/
2221+
@NonNull
2222+
private String buildStartsWithRegex(String prefix) {
2223+
return "^" + Pattern.quote(prefix);
2224+
}
21952225
}

Parse/src/test/java/com/parse/OfflineQueryLogicTest.java

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
*/
99
package com.parse;
1010

11+
import android.support.annotation.NonNull;
12+
1113
import org.json.JSONArray;
1214
import org.json.JSONObject;
1315
import org.junit.After;
@@ -23,6 +25,7 @@
2325
import java.util.HashMap;
2426
import java.util.List;
2527
import java.util.Map;
28+
import java.util.regex.Pattern;
2629

2730
import bolts.Task;
2831

@@ -419,6 +422,112 @@ public void testMatchesAll() throws Exception {
419422
assertFalse(matches(logic, query, object));
420423
}
421424

425+
@Test
426+
public void testMatchesAllStartingWith() throws Exception {
427+
ParseObject object = new ParseObject("TestObject");
428+
object.put("foo", Arrays.asList("foo", "bar"));
429+
430+
ParseQuery.State<ParseObject> query;
431+
OfflineQueryLogic logic = new OfflineQueryLogic(null);
432+
433+
query = new ParseQuery.State.Builder<>("TestObject")
434+
.addCondition("foo", "$all",
435+
Arrays.asList(
436+
buildStartsWithRegexKeyConstraint("foo"),
437+
buildStartsWithRegexKeyConstraint("bar")))
438+
.build();
439+
assertTrue(matches(logic, query, object));
440+
441+
query = new ParseQuery.State.Builder<>("TestObject")
442+
.addCondition("foo", "$all",
443+
Arrays.asList(
444+
buildStartsWithRegexKeyConstraint("fo"),
445+
buildStartsWithRegexKeyConstraint("b")))
446+
.build();
447+
assertTrue(matches(logic, query, object));
448+
449+
query = new ParseQuery.State.Builder<>("TestObject")
450+
.addCondition("foo", "$all",
451+
Arrays.asList(
452+
buildStartsWithRegexKeyConstraint("foo"),
453+
buildStartsWithRegexKeyConstraint("bar"),
454+
buildStartsWithRegexKeyConstraint("qux")))
455+
.build();
456+
assertFalse(matches(logic, query, object));
457+
458+
// Non-existant key
459+
object = new ParseObject("TestObject");
460+
assertFalse(matches(logic, query, object));
461+
object.put("foo", JSONObject.NULL);
462+
assertFalse(matches(logic, query, object));
463+
464+
thrown.expect(IllegalArgumentException.class);
465+
object.put("foo", "bar");
466+
assertFalse(matches(logic, query, object));
467+
}
468+
469+
@Test
470+
public void testMatchesAllStartingWithParameters() throws Exception {
471+
ParseObject object = new ParseObject("TestObject");
472+
object.put("foo", Arrays.asList("foo", "bar"));
473+
474+
ParseQuery.State<ParseObject> query;
475+
OfflineQueryLogic logic = new OfflineQueryLogic(null);
476+
477+
query = new ParseQuery.State.Builder<>("TestObject")
478+
.addCondition("foo", "$all",
479+
Arrays.asList(
480+
buildStartsWithRegexKeyConstraint("foo"),
481+
buildStartsWithRegexKeyConstraint("bar")))
482+
.build();
483+
assertTrue(matches(logic, query, object));
484+
485+
query = new ParseQuery.State.Builder<>("TestObject")
486+
.addCondition("foo", "$all",
487+
Arrays.asList(
488+
buildStartsWithRegexKeyConstraint("fo"),
489+
buildStartsWithRegex("ba"),
490+
"b"))
491+
.build();
492+
thrown.expect(IllegalArgumentException.class);
493+
assertFalse(matches(logic, query, object));
494+
495+
query = new ParseQuery.State.Builder<>("TestObject")
496+
.addCondition("foo", "$all",
497+
Arrays.asList(
498+
buildStartsWithRegexKeyConstraint("fo"),
499+
"b"))
500+
.build();
501+
thrown.expect(IllegalArgumentException.class);
502+
assertFalse(matches(logic, query, object));
503+
}
504+
505+
/**
506+
* Helper method to convert a string to a key constraint to match strings that starts with given
507+
* string.
508+
*
509+
* @param prefix String to use as prefix in regex.
510+
* @return The key constraint for word matching at the beginning of a string.
511+
*/
512+
@NonNull
513+
private ParseQuery.KeyConstraints buildStartsWithRegexKeyConstraint(String prefix) {
514+
ParseQuery.KeyConstraints constraint = new ParseQuery.KeyConstraints();
515+
constraint.put("$regex", buildStartsWithRegex(prefix));
516+
return constraint;
517+
}
518+
519+
/**
520+
* Helper method to convert a string to regex for start word matching.
521+
*
522+
* @param prefix String to use as prefix in regex.
523+
* @return The string converted as regex for start word matching.
524+
*/
525+
@NonNull
526+
private String buildStartsWithRegex(String prefix) {
527+
return "^" + Pattern.quote(prefix);
528+
}
529+
530+
422531
@Test
423532
public void testMatchesNearSphere() throws Exception {
424533
ParseGeoPoint fb = new ParseGeoPoint(37.481689f, -122.154949f);

Parse/src/test/java/com/parse/ParseQueryTest.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
*/
99
package com.parse;
1010

11+
import android.support.annotation.NonNull;
12+
1113
import org.junit.After;
1214
import org.junit.Before;
1315
import org.junit.Test;
@@ -389,6 +391,25 @@ public void testWhereContainsAll() throws Exception {
389391
verifyCondition(query, "key", "$all", values);
390392
}
391393

394+
@Test
395+
public void testWhereContainsAllStartingWith() throws Exception {
396+
ParseQuery<ParseObject> query = new ParseQuery<>("Test");
397+
String value = "value";
398+
String valueAgain = "valueAgain";
399+
List<String> values = Arrays.asList(value, valueAgain);
400+
401+
ParseQuery.KeyConstraints valueConverted = new ParseQuery.KeyConstraints();
402+
valueConverted.put("$regex", buildStartsWithPattern(value));
403+
ParseQuery.KeyConstraints valueAgainConverted = new ParseQuery.KeyConstraints();
404+
valueAgainConverted.put("$regex", buildStartsWithPattern(valueAgain));
405+
List<ParseQuery.KeyConstraints> valuesConverted =
406+
Arrays.asList(valueConverted, valueAgainConverted);
407+
408+
query.whereContainsAllStartsWith("key", values);
409+
410+
verifyCondition(query, "key", "$all", valuesConverted);
411+
}
412+
392413
@Test
393414
public void testWhereNotContainedIn() throws Exception {
394415
ParseQuery<ParseObject> query = new ParseQuery<>("Test");
@@ -425,7 +446,7 @@ public void testWhereStartsWith() throws Exception {
425446
String value = "prefix";
426447
query.whereStartsWith("key", value);
427448

428-
verifyCondition(query, "key", "$regex", "^" + Pattern.quote(value));
449+
verifyCondition(query, "key", "$regex", buildStartsWithPattern(value));
429450
}
430451

431452
@Test
@@ -818,7 +839,7 @@ private static void verifyCondition(
818839
assertEquals(map.get(constraintKey), values.get(constraintKey));
819840
}
820841
}
821-
842+
822843
//endregion
823844

824845
/**
@@ -904,4 +925,9 @@ public Task<Void> then(Task<Void> task) throws Exception {
904925
})).cast();
905926
}
906927
}
928+
929+
@NonNull
930+
private String buildStartsWithPattern(String value) {
931+
return "^" + Pattern.quote(value);
932+
}
907933
}

0 commit comments

Comments
 (0)