Skip to content

New query condition to match all strings that starts with some other given strings #673

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 98 additions & 6 deletions Parse/src/main/java/com/parse/OfflineQueryLogic.java
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,24 @@ private static boolean matchesEqualConstraint(Object constraint, Object value) {
return lhs.equals(rhs);
}

return compare(constraint, value, new Decider() {
@Override
public boolean decide(Object constraint, Object value) {
return constraint.equals(value);
}
});
Decider decider;
if (isStartsWithRegex(constraint)) {
decider = new Decider() {
@Override
public boolean decide(Object constraint, Object value) {
return ((String) value).matches(((KeyConstraints)constraint).get("$regex").toString());
}
};
} else {
decider = new Decider() {
@Override
public boolean decide(Object constraint, Object value) {
return constraint.equals(value);
}
};
}

return compare(constraint, value, decider);
}

/**
Expand Down Expand Up @@ -348,6 +360,13 @@ private static boolean matchesAllConstraint(Object constraint, Object value) {
}

if (constraint instanceof Collection) {
if (isAnyValueRegexStartsWith((Collection<?>) constraint)) {
constraint = cleanRegexStartsWith((Collection<?>) constraint);
if (constraint == null) {
throw new IllegalArgumentException("All values in $all queries must be of starting with regex or non regex.");
}
}

for (Object requiredItem : (Collection<?>) constraint) {
if (!matchesEqualConstraint(requiredItem, value)) {
return false;
Expand All @@ -358,6 +377,79 @@ private static boolean matchesAllConstraint(Object constraint, Object value) {
throw new IllegalArgumentException("Constraint type not supported for $all queries.");
}

/**
* Check if any of the collection constraints is a regex to match strings that starts with another
* string.
*/
private static boolean isAnyValueRegexStartsWith(Collection<?> constraints) {
for (Object constraint : constraints) {
if (isStartsWithRegex(constraint)) {
return true;
}
};

return false;
}

/**
* Cleans all regex constraints. If any of the constraints is not a regex, then null is returned.
* All values in a $all constraint must be a starting with another string regex.
*/
private static Collection<?> cleanRegexStartsWith(Collection<?> constraints) {
ArrayList<KeyConstraints> cleanedValues = new ArrayList<>();
for (Object constraint : constraints) {
if (!(constraint instanceof KeyConstraints)) {
return null;
}

KeyConstraints cleanedRegex = cleanRegexStartsWith((KeyConstraints) constraint);
if (cleanedRegex == null) {
return null;
}

cleanedValues.add(cleanedRegex);
}

return cleanedValues;
}

/**
* Creates a regex pattern to match a substring at the beginning of another string.
*
* If given string is not a regex to match a string at the beginning of another string, then null
* is returned.
*/
private static KeyConstraints cleanRegexStartsWith(KeyConstraints regex) {
if (!isStartsWithRegex(regex)) {
return null;
}

// remove all instances of \Q and \E from the remaining text & escape single quotes
String literalizedString = ((String)regex.get("$regex"))
.replaceAll("([^\\\\])(\\\\E)", "$1")
.replaceAll("([^\\\\])(\\\\Q)", "$1")
.replaceAll("^\\\\E", "")
.replaceAll("^\\\\Q", "")
.replaceAll("([^'])'", "$1''")
.replaceAll("^'([^'])", "''$1");

regex.put("$regex", literalizedString + ".*");
return regex;
}

/**
* Check if given constraint is a regex to match strings that starts with another string.
*/
private static boolean isStartsWithRegex(Object constraint) {
if (constraint == null || !(constraint instanceof KeyConstraints)) {
return false;
}

KeyConstraints keyConstraints = (KeyConstraints) constraint;
return keyConstraints.size() == 1 && keyConstraints.containsKey("$regex") &&
((String)keyConstraints.get("$regex")).startsWith("^");
}

/**
* Matches $regex constraints.
*/
Expand Down
40 changes: 35 additions & 5 deletions Parse/src/main/java/com/parse/ParseQuery.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
*/
package com.parse;

import android.support.annotation.NonNull;

import org.json.JSONException;
import org.json.JSONObject;

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

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

/**
* Add a constraint to the query that requires a particular key's value to contain each one of
* the provided list of strings entirely or just starting with given strings.
*
* @param key
* The key to check. This key's value must be an array.
* @param values
* The values that will match entirely or starting with them.
* @return this, so you can chain this call.
*/
public ParseQuery<T> whereContainsAllStartsWith(String key, Collection<String> values) {
ArrayList<KeyConstraints> startsWithConstraints = new ArrayList<>();
for (String value : values) {
KeyConstraints keyConstraints = new KeyConstraints();
keyConstraints.put("$regex", buildStartsWithRegex(value));
startsWithConstraints.add(keyConstraints);
}

return whereContainsAll(key, startsWithConstraints);
}

/**
* Add a constraint to the query that requires a particular key's value match another
* {@code ParseQuery}.
Expand Down Expand Up @@ -1988,7 +2007,7 @@ public ParseQuery<T> whereContains(String key, String substring) {
* @return this, so you can chain this call.
*/
public ParseQuery<T> whereStartsWith(String key, String prefix) {
String regex = "^" + Pattern.quote(prefix);
String regex = buildStartsWithRegex(prefix);
whereMatches(key, regex);
return this;
}
Expand Down Expand Up @@ -2192,4 +2211,15 @@ public ParseQuery<T> setTrace(boolean shouldTrace) {
builder.setTracingEnabled(shouldTrace);
return this;
}

/**
* Helper method to convert a string to regex for start word matching.
*
* @param prefix String to use as prefix in regex.
* @return The string converted as regex for start word matching.
*/
@NonNull
private String buildStartsWithRegex(String prefix) {
return "^" + Pattern.quote(prefix);
}
}
109 changes: 109 additions & 0 deletions Parse/src/test/java/com/parse/OfflineQueryLogicTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
*/
package com.parse;

import android.support.annotation.NonNull;

import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.After;
Expand All @@ -23,6 +25,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import bolts.Task;

Expand Down Expand Up @@ -419,6 +422,112 @@ public void testMatchesAll() throws Exception {
assertFalse(matches(logic, query, object));
}

@Test
public void testMatchesAllStartingWith() throws Exception {
ParseObject object = new ParseObject("TestObject");
object.put("foo", Arrays.asList("foo", "bar"));

ParseQuery.State<ParseObject> query;
OfflineQueryLogic logic = new OfflineQueryLogic(null);

query = new ParseQuery.State.Builder<>("TestObject")
.addCondition("foo", "$all",
Arrays.asList(
buildStartsWithRegexKeyConstraint("foo"),
buildStartsWithRegexKeyConstraint("bar")))
.build();
assertTrue(matches(logic, query, object));

query = new ParseQuery.State.Builder<>("TestObject")
.addCondition("foo", "$all",
Arrays.asList(
buildStartsWithRegexKeyConstraint("fo"),
buildStartsWithRegexKeyConstraint("b")))
.build();
assertTrue(matches(logic, query, object));

query = new ParseQuery.State.Builder<>("TestObject")
.addCondition("foo", "$all",
Arrays.asList(
buildStartsWithRegexKeyConstraint("foo"),
buildStartsWithRegexKeyConstraint("bar"),
buildStartsWithRegexKeyConstraint("qux")))
.build();
assertFalse(matches(logic, query, object));

// Non-existant key
object = new ParseObject("TestObject");
assertFalse(matches(logic, query, object));
object.put("foo", JSONObject.NULL);
assertFalse(matches(logic, query, object));

thrown.expect(IllegalArgumentException.class);
object.put("foo", "bar");
assertFalse(matches(logic, query, object));
}

@Test
public void testMatchesAllStartingWithParameters() throws Exception {
ParseObject object = new ParseObject("TestObject");
object.put("foo", Arrays.asList("foo", "bar"));

ParseQuery.State<ParseObject> query;
OfflineQueryLogic logic = new OfflineQueryLogic(null);

query = new ParseQuery.State.Builder<>("TestObject")
.addCondition("foo", "$all",
Arrays.asList(
buildStartsWithRegexKeyConstraint("foo"),
buildStartsWithRegexKeyConstraint("bar")))
.build();
assertTrue(matches(logic, query, object));

query = new ParseQuery.State.Builder<>("TestObject")
.addCondition("foo", "$all",
Arrays.asList(
buildStartsWithRegexKeyConstraint("fo"),
buildStartsWithRegex("ba"),
"b"))
.build();
thrown.expect(IllegalArgumentException.class);
assertFalse(matches(logic, query, object));

query = new ParseQuery.State.Builder<>("TestObject")
.addCondition("foo", "$all",
Arrays.asList(
buildStartsWithRegexKeyConstraint("fo"),
"b"))
.build();
thrown.expect(IllegalArgumentException.class);
assertFalse(matches(logic, query, object));
}

/**
* Helper method to convert a string to a key constraint to match strings that starts with given
* string.
*
* @param prefix String to use as prefix in regex.
* @return The key constraint for word matching at the beginning of a string.
*/
@NonNull
private ParseQuery.KeyConstraints buildStartsWithRegexKeyConstraint(String prefix) {
ParseQuery.KeyConstraints constraint = new ParseQuery.KeyConstraints();
constraint.put("$regex", buildStartsWithRegex(prefix));
return constraint;
}

/**
* Helper method to convert a string to regex for start word matching.
*
* @param prefix String to use as prefix in regex.
* @return The string converted as regex for start word matching.
*/
@NonNull
private String buildStartsWithRegex(String prefix) {
return "^" + Pattern.quote(prefix);
}


@Test
public void testMatchesNearSphere() throws Exception {
ParseGeoPoint fb = new ParseGeoPoint(37.481689f, -122.154949f);
Expand Down
28 changes: 27 additions & 1 deletion Parse/src/test/java/com/parse/ParseQueryTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
*/
package com.parse;

import android.support.annotation.NonNull;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
Expand Down Expand Up @@ -389,6 +391,25 @@ public void testWhereContainsAll() throws Exception {
verifyCondition(query, "key", "$all", values);
}

@Test
public void testWhereContainsAllStartingWith() throws Exception {
ParseQuery<ParseObject> query = new ParseQuery<>("Test");
String value = "value";
String valueAgain = "valueAgain";
List<String> values = Arrays.asList(value, valueAgain);

ParseQuery.KeyConstraints valueConverted = new ParseQuery.KeyConstraints();
valueConverted.put("$regex", buildStartsWithPattern(value));
ParseQuery.KeyConstraints valueAgainConverted = new ParseQuery.KeyConstraints();
valueAgainConverted.put("$regex", buildStartsWithPattern(valueAgain));
List<ParseQuery.KeyConstraints> valuesConverted =
Arrays.asList(valueConverted, valueAgainConverted);

query.whereContainsAllStartsWith("key", values);

verifyCondition(query, "key", "$all", valuesConverted);
}

@Test
public void testWhereNotContainedIn() throws Exception {
ParseQuery<ParseObject> query = new ParseQuery<>("Test");
Expand Down Expand Up @@ -425,7 +446,7 @@ public void testWhereStartsWith() throws Exception {
String value = "prefix";
query.whereStartsWith("key", value);

verifyCondition(query, "key", "$regex", "^" + Pattern.quote(value));
verifyCondition(query, "key", "$regex", buildStartsWithPattern(value));
}

@Test
Expand Down Expand Up @@ -904,4 +925,9 @@ public Task<Void> then(Task<Void> task) throws Exception {
})).cast();
}
}

@NonNull
private String buildStartsWithPattern(String value) {
return "^" + Pattern.quote(value);
}
}