ParseApplication/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/OfflineQueryLogic.java

1120 lines
37 KiB
Java

/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import com.parse.ParseQuery.KeyConstraints;
import com.parse.ParseQuery.QueryConstraints;
import com.parse.ParseQuery.RelationConstraint;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import bolts.Continuation;
import bolts.Task;
/** package */ class OfflineQueryLogic {
/**
* A query is converted into a complex hierarchy of ConstraintMatchers that evaluate whether a
* ParseObject matches each part of the query. This is done because some parts of the query (such
* as $inQuery) are much more efficient if we can do some preprocessing. This makes some parts of
* the query matching stateful.
*/
/* package */ abstract class ConstraintMatcher<T extends ParseObject> {
/* package */ final ParseUser user;
public ConstraintMatcher(ParseUser user) {
this.user = user;
}
/* package */ abstract Task<Boolean> matchesAsync(T object, ParseSQLiteDatabase db);
}
private final OfflineStore store;
/* package */ OfflineQueryLogic(OfflineStore store) {
this.store = store;
}
/**
* Returns an Object's value for a given key, handling any special keys like objectId. Also
* handles dot-notation for traversing into objects.
*/
private static Object getValue(Object container, String key) throws ParseException {
return getValue(container, key, 0);
}
private static Object getValue(Object container, String key, int depth) throws ParseException {
if (key.contains(".")) {
String[] parts = key.split("\\.", 2);
Object value = getValue(container, parts[0], depth + 1);
/*
* Only Maps and JSONObjects can be dotted into for getting values, so we should reject
* anything like ParseObjects and arrays.
*/
if (!(value == null || value == JSONObject.NULL || value instanceof Map || value instanceof JSONObject)) {
// Technically, they can search inside the REST representation of some nested objects.
if (depth > 0) {
Object restFormat = null;
try {
restFormat = PointerEncoder.get().encode(value);
} catch (Exception e) {
// Well, if we couldn't encode it, it's not searchable.
}
if (restFormat instanceof JSONObject) {
return getValue(restFormat, parts[1], depth + 1);
}
}
throw new ParseException(ParseException.INVALID_QUERY, String.format("Key %s is invalid.",
key));
}
return getValue(value, parts[1], depth + 1);
}
if (container instanceof ParseObject) {
final ParseObject object = (ParseObject) container;
// The object needs to have been fetched already if we are going to sort by one of its fields.
if (!object.isDataAvailable()) {
throw new ParseException(ParseException.INVALID_NESTED_KEY, String.format("Bad key: %s",
key));
}
// Handle special keys for ParseObjects.
switch (key) {
case "objectId":
return object.getObjectId();
case "createdAt":
case "_created_at":
return object.getCreatedAt();
case "updatedAt":
case "_updated_at":
return object.getUpdatedAt();
default:
return object.get(key);
}
} else if (container instanceof JSONObject) {
return ((JSONObject) container).opt(key);
} else if (container instanceof Map) {
return ((Map<?, ?>) container).get(key);
} else if (container == JSONObject.NULL) {
return null;
} else if (container == null) {
return null;
} else {
throw new ParseException(ParseException.INVALID_NESTED_KEY, String.format("Bad key: %s", key));
}
}
/**
* General purpose compareTo that figures out the right types to use. The arguments should be
* atomic values to compare, such as Dates, Strings, or Numbers -- not composite objects or
* arrays.
*/
private static int compareTo(Object lhs, Object rhs) {
boolean lhsIsNullOrUndefined = (lhs == JSONObject.NULL || lhs == null);
boolean rhsIsNullOrUndefined = (rhs == JSONObject.NULL || rhs == null);
if (lhsIsNullOrUndefined || rhsIsNullOrUndefined) {
if (!lhsIsNullOrUndefined) {
return 1;
} else if (!rhsIsNullOrUndefined) {
return -1;
} else {
return 0;
}
} else if (lhs instanceof Date && rhs instanceof Date) {
return ((Date) lhs).compareTo((Date) rhs);
} else if (lhs instanceof String && rhs instanceof String) {
return ((String) lhs).compareTo((String) rhs);
} else if (lhs instanceof Number && rhs instanceof Number) {
return Numbers.compare((Number) lhs, (Number) rhs);
} else {
throw new IllegalArgumentException(
String.format("Cannot compare %s against %s", lhs, rhs));
}
}
/**
* A decider decides whether the given value matches the given constraint.
*/
private interface Decider {
boolean decide(Object constraint, Object value);
}
/**
* Returns true if decider returns true for any value in the given list.
*/
private static boolean compareList(Object constraint, List<?> values, Decider decider) {
for (Object value : values) {
if (decider.decide(constraint, value)) {
return true;
}
}
return false;
}
/**
* Returns true if decider returns true for any value in the given list.
*/
private static boolean compareArray(Object constraint, JSONArray values, Decider decider) {
for (int i = 0; i < values.length(); ++i) {
try {
if (decider.decide(constraint, values.get(i))) {
return true;
}
} catch (JSONException e) {
// This can literally never happen.
throw new RuntimeException(e);
}
}
return false;
}
/**
*
* Returns true if the decider returns true for the given value and the given constraint. This
* method handles Mongo's logic where an item can match either an item itself, or any item within
* the item, if the item is an array.
*/
private static boolean compare(Object constraint, Object value, Decider decider) {
if (value instanceof List) {
return compareList(constraint, (List<?>) value, decider);
} else if (value instanceof JSONArray) {
return compareArray(constraint, (JSONArray) value, decider);
} else {
return decider.decide(constraint, value);
}
}
/**
* Implements simple equality constraints. This emulates Mongo's behavior where "equals" can mean
* array containment.
*/
private static boolean matchesEqualConstraint(Object constraint, Object value) {
if (constraint == null || value == null) {
return constraint == value;
}
if (constraint instanceof Number && value instanceof Number) {
return compareTo(constraint, value) == 0;
}
if (constraint instanceof ParseGeoPoint && value instanceof ParseGeoPoint) {
ParseGeoPoint lhs = (ParseGeoPoint) constraint;
ParseGeoPoint rhs = (ParseGeoPoint) value;
return lhs.getLatitude() == rhs.getLatitude()
&& lhs.getLongitude() == rhs.getLongitude();
}
if (constraint instanceof ParsePolygon && value instanceof ParsePolygon) {
ParsePolygon lhs = (ParsePolygon) constraint;
ParsePolygon rhs = (ParsePolygon) value;
return lhs.equals(rhs);
}
return compare(constraint, value, new Decider() {
@Override
public boolean decide(Object constraint, Object value) {
return constraint.equals(value);
}
});
}
/**
* Matches $ne constraints.
*/
private static boolean matchesNotEqualConstraint(Object constraint, Object value) {
return !matchesEqualConstraint(constraint, value);
}
/**
* Matches $lt constraints.
*/
private static boolean matchesLessThanConstraint(Object constraint, Object value) {
return compare(constraint, value, new Decider() {
@Override
public boolean decide(Object constraint, Object value) {
if (value == null || value == JSONObject.NULL) {
return false;
}
return compareTo(constraint, value) > 0;
}
});
}
/**
* Matches $lte constraints.
*/
private static boolean matchesLessThanOrEqualToConstraint(Object constraint, Object value) {
return compare(constraint, value, new Decider() {
@Override
public boolean decide(Object constraint, Object value) {
if (value == null || value == JSONObject.NULL) {
return false;
}
return compareTo(constraint, value) >= 0;
}
});
}
/**
* Matches $gt constraints.
*/
private static boolean matchesGreaterThanConstraint(Object constraint, Object value) {
return compare(constraint, value, new Decider() {
@Override
public boolean decide(Object constraint, Object value) {
if (value == null || value == JSONObject.NULL) {
return false;
}
return compareTo(constraint, value) < 0;
}
});
}
/**
* Matches $gte constraints.
*/
private static boolean matchesGreaterThanOrEqualToConstraint(Object constraint, Object value) {
return compare(constraint, value, new Decider() {
@Override
public boolean decide(Object constraint, Object value) {
if (value == null || value == JSONObject.NULL) {
return false;
}
return compareTo(constraint, value) <= 0;
}
});
}
/**
* Matches $in constraints.
* $in returns true if the intersection of value and constraint is not an empty set.
*/
private static boolean matchesInConstraint(Object constraint, Object value) {
if (constraint instanceof Collection) {
for (Object requiredItem : (Collection<?>)constraint) {
if (matchesEqualConstraint(requiredItem, value)) {
return true;
}
}
return false;
}
throw new IllegalArgumentException("Constraint type not supported for $in queries.");
}
/**
* Matches $nin constraints.
*/
private static boolean matchesNotInConstraint(Object constraint, Object value) {
return !matchesInConstraint(constraint, value);
}
/**
* Matches $all constraints.
*/
private static boolean matchesAllConstraint(Object constraint, Object value) {
if (value == null || value == JSONObject.NULL) {
return false;
}
if (!(value instanceof Collection)) {
throw new IllegalArgumentException("Value type not supported for $all queries.");
}
if (constraint instanceof Collection) {
for (Object requiredItem : (Collection<?>) constraint) {
if (!matchesEqualConstraint(requiredItem, value)) {
return false;
}
}
return true;
}
throw new IllegalArgumentException("Constraint type not supported for $all queries.");
}
/**
* Matches $regex constraints.
*/
private static boolean matchesRegexConstraint(Object constraint, Object value, String options)
throws ParseException {
if (value == null || value == JSONObject.NULL) {
return false;
}
if (options == null) {
options = "";
}
if (!options.matches("^[imxs]*$")) {
throw new ParseException(ParseException.INVALID_QUERY, String.format(
"Invalid regex options: %s", options));
}
int flags = 0;
if (options.contains("i")) {
flags = flags | Pattern.CASE_INSENSITIVE;
}
if (options.contains("m")) {
flags = flags | Pattern.MULTILINE;
}
if (options.contains("x")) {
flags = flags | Pattern.COMMENTS;
}
if (options.contains("s")) {
flags = flags | Pattern.DOTALL;
}
String regex = (String) constraint;
Pattern pattern = Pattern.compile(regex, flags);
Matcher matcher = pattern.matcher((String) value);
return matcher.find();
}
/**
* Matches $exists constraints.
*/
private static boolean matchesExistsConstraint(Object constraint, Object value) {
/*
* In the Android SDK, null means "undefined", and JSONObject.NULL means "null".
*/
if (constraint != null && (Boolean) constraint) {
return value != null && value != JSONObject.NULL;
} else {
return value == null || value == JSONObject.NULL;
}
}
/**
* Matches $nearSphere constraints.
*/
private static boolean matchesNearSphereConstraint(Object constraint, Object value,
Double maxDistance) {
if (value == null || value == JSONObject.NULL) {
return false;
}
if (maxDistance == null) {
return true;
}
ParseGeoPoint point1 = (ParseGeoPoint) constraint;
ParseGeoPoint point2 = (ParseGeoPoint) value;
return point1.distanceInRadiansTo(point2) <= maxDistance;
}
/**
* Matches $within constraints.
*/
private static boolean matchesWithinConstraint(Object constraint, Object value)
throws ParseException {
if (value == null || value == JSONObject.NULL) {
return false;
}
@SuppressWarnings("unchecked")
HashMap<String, ArrayList<ParseGeoPoint>> constraintMap =
(HashMap<String, ArrayList<ParseGeoPoint>>) constraint;
ArrayList<ParseGeoPoint> box = constraintMap.get("$box");
ParseGeoPoint southwest = box.get(0);
ParseGeoPoint northeast = box.get(1);
ParseGeoPoint target = (ParseGeoPoint) value;
if (northeast.getLongitude() < southwest.getLongitude()) {
throw new ParseException(ParseException.INVALID_QUERY,
"whereWithinGeoBox queries cannot cross the International Date Line.");
}
if (northeast.getLatitude() < southwest.getLatitude()) {
throw new ParseException(ParseException.INVALID_QUERY,
"The southwest corner of a geo box must be south of the northeast corner.");
}
if (northeast.getLongitude() - southwest.getLongitude() > 180) {
throw new ParseException(ParseException.INVALID_QUERY,
"Geo box queries larger than 180 degrees in longitude are not supported. "
+ "Please check point order.");
}
return (target.getLatitude() >= southwest.getLatitude()
&& target.getLatitude() <= northeast.getLatitude()
&& target.getLongitude() >= southwest.getLongitude()
&& target.getLongitude() <= northeast.getLongitude());
}
/**
* Matches $geoIntersects constraints.
*/
private static boolean matchesGeoIntersectsConstraint(Object constraint, Object value)
throws ParseException {
if (value == null || value == JSONObject.NULL) {
return false;
}
@SuppressWarnings("unchecked")
HashMap<String, ParseGeoPoint> constraintMap =
(HashMap<String, ParseGeoPoint>) constraint;
ParseGeoPoint point = constraintMap.get("$point");
ParsePolygon target = (ParsePolygon) value;
return target.containsPoint(point);
}
/**
* Matches $geoWithin constraints.
*/
private static boolean matchesGeoWithinConstraint(Object constraint, Object value)
throws ParseException {
if (value == null || value == JSONObject.NULL) {
return false;
}
@SuppressWarnings("unchecked")
HashMap<String, List<ParseGeoPoint>> constraintMap =
(HashMap<String, List<ParseGeoPoint>>) constraint;
List<ParseGeoPoint> points = constraintMap.get("$polygon");
ParsePolygon polygon = new ParsePolygon(points);
ParseGeoPoint point = (ParseGeoPoint) value;
return polygon.containsPoint(point);
}
/**
* Returns true iff the given value matches the given operator and constraint.
*
* @throws UnsupportedOperationException
* if the operator is not one this function can handle.
*/
private static boolean matchesStatelessConstraint(String operator, Object constraint,
Object value, KeyConstraints allKeyConstraints) throws ParseException {
switch (operator) {
case "$ne":
return matchesNotEqualConstraint(constraint, value);
case "$lt":
return matchesLessThanConstraint(constraint, value);
case "$lte":
return matchesLessThanOrEqualToConstraint(constraint, value);
case "$gt":
return matchesGreaterThanConstraint(constraint, value);
case "$gte":
return matchesGreaterThanOrEqualToConstraint(constraint, value);
case "$in":
return matchesInConstraint(constraint, value);
case "$nin":
return matchesNotInConstraint(constraint, value);
case "$all":
return matchesAllConstraint(constraint, value);
case "$regex":
String regexOptions = (String) allKeyConstraints.get("$options");
return matchesRegexConstraint(constraint, value, regexOptions);
case "$options":
// No need to do anything. This is handled by $regex.
return true;
case "$exists":
return matchesExistsConstraint(constraint, value);
case "$nearSphere":
Double maxDistance = (Double) allKeyConstraints.get("$maxDistance");
return matchesNearSphereConstraint(constraint, value, maxDistance);
case "$maxDistance":
// No need to do anything. This is handled by $nearSphere.
return true;
case "$within":
return matchesWithinConstraint(constraint, value);
case "$geoWithin":
return matchesGeoWithinConstraint(constraint, value);
case "$geoIntersects":
return matchesGeoIntersectsConstraint(constraint, value);
default:
throw new UnsupportedOperationException(String.format(
"The offline store does not yet support the %s operator.", operator));
}
}
private abstract class SubQueryMatcher<T extends ParseObject> extends ConstraintMatcher<T> {
private final ParseQuery.State<T> subQuery;
private Task<List<T>> subQueryResults = null;
public SubQueryMatcher(ParseUser user, ParseQuery.State<T> subQuery) {
super(user);
this.subQuery = subQuery;
}
@Override
public Task<Boolean> matchesAsync(final T object, ParseSQLiteDatabase db) {
/*
* As an optimization, we do this lazily. Then we may not have to do it at all, if this part
* of the query gets short-circuited.
*/
if (subQueryResults == null) {
//TODO (grantland): We need to pass through the original pin we were limiting the parent
// query on.
subQueryResults = store.findAsync(subQuery, user, null, db);
}
return subQueryResults.onSuccess(new Continuation<List<T>, Boolean>() {
@Override
public Boolean then(Task<List<T>> task) throws ParseException {
return matches(object, task.getResult());
}
});
}
protected abstract boolean matches(T object, List<T> results) throws ParseException;
}
/**
* Creates a matcher that handles $inQuery constraints.
*/
private <T extends ParseObject> ConstraintMatcher<T> createInQueryMatcher(ParseUser user,
Object constraint, final String key) {
// TODO(grantland): Convert builder to state t6941155
@SuppressWarnings("unchecked")
ParseQuery.State<T> query = ((ParseQuery.State.Builder<T>) constraint).build();
return new SubQueryMatcher<T>(user, query) {
@Override
protected boolean matches(T object, List<T> results) throws ParseException {
Object value = getValue(object, key);
return matchesInConstraint(results, value);
}
};
}
/**
* Creates a matcher that handles $notInQuery constraints.
*/
private <T extends ParseObject> ConstraintMatcher<T> createNotInQueryMatcher(ParseUser user,
Object constraint, final String key) {
final ConstraintMatcher<T> inQueryMatcher = createInQueryMatcher(user, constraint, key);
return new ConstraintMatcher<T>(user) {
@Override
public Task<Boolean> matchesAsync(T object, ParseSQLiteDatabase db) {
return inQueryMatcher.matchesAsync(object, db).onSuccess(new Continuation<Boolean, Boolean>() {
@Override
public Boolean then(Task<Boolean> task) throws Exception {
return !task.getResult();
}
});
}
};
}
/**
* Creates a matcher that handles $select constraints.
*/
private <T extends ParseObject> ConstraintMatcher<T> createSelectMatcher(ParseUser user,
Object constraint, final String key) {
Map<?, ?> constraintMap = (Map<?, ?>) constraint;
// TODO(grantland): Convert builder to state t6941155
@SuppressWarnings("unchecked")
ParseQuery.State<T> query = ((ParseQuery.State.Builder<T>) constraintMap.get("query")).build();
final String resultKey = (String) constraintMap.get("key");
return new SubQueryMatcher<T>(user, query) {
@Override
protected boolean matches(T object, List<T> results) throws ParseException {
Object value = getValue(object, key);
for (T result : results) {
Object resultValue = getValue(result, resultKey);
if (matchesEqualConstraint(value, resultValue)) {
return true;
}
}
return false;
}
};
}
/**
* Creates a matcher that handles $dontSelect constraints.
*/
private <T extends ParseObject> ConstraintMatcher<T> createDontSelectMatcher(ParseUser user,
Object constraint, final String key) {
final ConstraintMatcher<T> selectMatcher = createSelectMatcher(user, constraint, key);
return new ConstraintMatcher<T>(user) {
@Override
public Task<Boolean> matchesAsync(T object, ParseSQLiteDatabase db) {
return selectMatcher.matchesAsync(object, db).onSuccess(new Continuation<Boolean, Boolean>() {
@Override
public Boolean then(Task<Boolean> task) throws Exception {
return !task.getResult();
}
});
}
};
}
/*
* Creates a matcher for a particular constraint operator.
*/
private <T extends ParseObject> ConstraintMatcher<T> createMatcher(ParseUser user,
final String operator, final Object constraint, final String key,
final KeyConstraints allKeyConstraints) {
switch (operator) {
case "$inQuery":
return createInQueryMatcher(user, constraint, key);
case "$notInQuery":
return createNotInQueryMatcher(user, constraint, key);
case "$select":
return createSelectMatcher(user, constraint, key);
case "$dontSelect":
return createDontSelectMatcher(user, constraint, key);
default:
/*
* All of the other operators we know about are stateless, so return a simple matcher.
*/
return new ConstraintMatcher<T>(user) {
@Override
public Task<Boolean> matchesAsync(T object, ParseSQLiteDatabase db) {
try {
Object value = getValue(object, key);
return Task.forResult(matchesStatelessConstraint(operator, constraint, value,
allKeyConstraints));
} catch (ParseException e) {
return Task.forError(e);
}
}
};
}
}
/**
* Handles $or queries.
*/
private <T extends ParseObject> ConstraintMatcher<T> createOrMatcher(ParseUser user,
ArrayList<QueryConstraints> queries) {
// Make a list of all the matchers to OR together.
final ArrayList<ConstraintMatcher<T>> matchers = new ArrayList<>();
for (QueryConstraints constraints : queries) {
ConstraintMatcher<T> matcher = createMatcher(user, constraints);
matchers.add(matcher);
}
/*
* Now OR together the constraints for each query.
*/
return new ConstraintMatcher<T>(user) {
@Override
public Task<Boolean> matchesAsync(final T object, final ParseSQLiteDatabase db) {
Task<Boolean> task = Task.forResult(false);
for (final ConstraintMatcher<T> matcher : matchers) {
task = task.onSuccessTask(new Continuation<Boolean, Task<Boolean>>() {
@Override
public Task<Boolean> then(Task<Boolean> task) throws Exception {
if (task.getResult()) {
return task;
}
return matcher.matchesAsync(object, db);
}
});
}
return task;
}
};
}
/**
* Returns a ConstraintMatcher that return true iff the object matches QueryConstraints. This
* takes in a SQLiteDatabase connection because SQLite is finicky about nesting connections, so we
* want to reuse them whenever possible.
*/
private <T extends ParseObject> ConstraintMatcher<T> createMatcher(ParseUser user,
QueryConstraints queryConstraints) {
// Make a list of all the matchers to AND together.
final ArrayList<ConstraintMatcher<T>> matchers = new ArrayList<>();
for (final String key : queryConstraints.keySet()) {
final Object queryConstraintValue = queryConstraints.get(key);
if (key.equals("$or")) {
/*
* A set of queries to be OR-ed together.
*/
@SuppressWarnings("unchecked")
ConstraintMatcher<T> matcher =
createOrMatcher(user, (ArrayList<QueryConstraints>) queryConstraintValue);
matchers.add(matcher);
} else if (queryConstraintValue instanceof KeyConstraints) {
/*
* It's a set of constraints that should be AND-ed together.
*/
KeyConstraints keyConstraints = (KeyConstraints) queryConstraintValue;
for (String operator : keyConstraints.keySet()) {
final Object keyConstraintValue = keyConstraints.get(operator);
ConstraintMatcher<T> matcher =
createMatcher(user, operator, keyConstraintValue, key, keyConstraints);
matchers.add(matcher);
}
} else if (queryConstraintValue instanceof RelationConstraint) {
/*
* It's a $relatedTo constraint.
*/
final RelationConstraint relation = (RelationConstraint) queryConstraintValue;
matchers.add(new ConstraintMatcher<T>(user) {
@Override
public Task<Boolean> matchesAsync(T object, ParseSQLiteDatabase db) {
return Task.forResult(relation.getRelation().hasKnownObject(object));
}
});
} else {
/*
* It's not a set of constraints, so it's just a value to compare against.
*/
matchers.add(new ConstraintMatcher<T>(user) {
@Override
public Task<Boolean> matchesAsync(T object, ParseSQLiteDatabase db) {
Object objectValue;
try {
objectValue = getValue(object, key);
} catch (ParseException e) {
return Task.forError(e);
}
return Task.forResult(matchesEqualConstraint(queryConstraintValue, objectValue));
}
});
}
}
/*
* Now AND together the constraints for each key.
*/
return new ConstraintMatcher<T>(user) {
@Override
public Task<Boolean> matchesAsync(final T object, final ParseSQLiteDatabase db) {
Task<Boolean> task = Task.forResult(true);
for (final ConstraintMatcher<T> matcher : matchers) {
task = task.onSuccessTask(new Continuation<Boolean, Task<Boolean>>() {
@Override
public Task<Boolean> then(Task<Boolean> task) throws Exception {
if (!task.getResult()) {
return task;
}
return matcher.matchesAsync(object, db);
}
});
}
return task;
}
};
}
/**
* Returns true iff the object is visible based on its read ACL and the given user objectId.
*/
/* package */ static <T extends ParseObject> boolean hasReadAccess(ParseUser user, T object) {
if (user == object) {
return true;
}
ParseACL acl = object.getACL();
if (acl == null) {
return true;
}
if (acl.getPublicReadAccess()) {
return true;
}
if (user != null && acl.getReadAccess(user)) {
return true;
}
// TODO: Implement roles.
return false;
}
/**
* Returns true iff the object is visible based on its read ACL and the given user objectId.
*/
/* package */ static <T extends ParseObject> boolean hasWriteAccess(ParseUser user, T object) {
if (user == object) {
return true;
}
ParseACL acl = object.getACL();
if (acl == null) {
return true;
}
if (acl.getPublicWriteAccess()) {
return true;
}
if (user != null && acl.getWriteAccess(user)) {
return true;
}
// TODO: Implement roles.
return false;
}
/**
* Returns a ConstraintMatcher that return true iff the object matches the given query's
* constraints. This takes in a SQLiteDatabase connection because SQLite is finicky about nesting
* connections, so we want to reuse them whenever possible.
*
* @param state The query.
* @param user The user we are testing ACL access for.
* @param <T> Subclass of ParseObject.
* @return A new instance of ConstraintMatcher.
*/
/* package */ <T extends ParseObject> ConstraintMatcher<T> createMatcher(
ParseQuery.State<T> state, final ParseUser user) {
final boolean ignoreACLs = state.ignoreACLs();
final ConstraintMatcher<T> constraintMatcher = createMatcher(user, state.constraints());
return new ConstraintMatcher<T>(user) {
@Override
public Task<Boolean> matchesAsync(T object, ParseSQLiteDatabase db) {
if (!ignoreACLs && !hasReadAccess(user, object)) {
return Task.forResult(false);
}
return constraintMatcher.matchesAsync(object, db);
}
};
}
/**
* Sorts the given array based on the parameters of the given query.
*/
/* package */ static <T extends ParseObject> void sort(List<T> results, ParseQuery.State<T> state)
throws ParseException {
final List<String> keys = state.order();
// Do some error checking just for maximum compatibility with the server.
for (String key : state.order()) {
if (!key.matches("^-?[A-Za-z][A-Za-z0-9_]*$")) {
if (!"_created_at".equals(key) && !"_updated_at".equals(key)) {
throw new ParseException(ParseException.INVALID_KEY_NAME, String.format(
"Invalid key name: \"%s\".", key));
}
}
}
// See if there's a $nearSphere constraint that will override the other sort parameters.
String mutableNearSphereKey = null;
ParseGeoPoint mutableNearSphereValue = null;
for (String queryKey : state.constraints().keySet()) {
Object queryKeyConstraints = state.constraints().get(queryKey);
if (queryKeyConstraints instanceof KeyConstraints) {
KeyConstraints keyConstraints = (KeyConstraints) queryKeyConstraints;
if (keyConstraints.containsKey("$nearSphere")) {
mutableNearSphereKey = queryKey;
mutableNearSphereValue = (ParseGeoPoint) keyConstraints.get("$nearSphere");
}
}
}
final String nearSphereKey = mutableNearSphereKey;
final ParseGeoPoint nearSphereValue = mutableNearSphereValue;
// If there's nothing to sort based on, then don't do anything.
if (keys.size() == 0 && mutableNearSphereKey == null) {
return;
}
/*
* TODO(klimt): Test whether we allow dotting into objects for sorting.
*/
Collections.sort(results, new Comparator<T>() {
@Override
public int compare(T lhs, T rhs) {
if (nearSphereKey != null) {
ParseGeoPoint lhsPoint;
ParseGeoPoint rhsPoint;
try {
lhsPoint = (ParseGeoPoint) getValue(lhs, nearSphereKey);
rhsPoint = (ParseGeoPoint) getValue(rhs, nearSphereKey);
} catch (ParseException e) {
throw new RuntimeException(e);
}
// GeoPoints can't be null if there's a $nearSphere.
double lhsDistance = lhsPoint.distanceInRadiansTo(nearSphereValue);
double rhsDistance = rhsPoint.distanceInRadiansTo(nearSphereValue);
if (lhsDistance != rhsDistance) {
return (lhsDistance - rhsDistance > 0) ? 1 : -1;
}
}
for (String key : keys) {
boolean descending = false;
if (key.startsWith("-")) {
descending = true;
key = key.substring(1);
}
Object lhsValue;
Object rhsValue;
try {
lhsValue = getValue(lhs, key);
rhsValue = getValue(rhs, key);
} catch (ParseException e) {
throw new RuntimeException(e);
}
int result;
try {
result = compareTo(lhsValue, rhsValue);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(String.format("Unable to sort by key %s.", key), e);
}
if (result != 0) {
return descending ? -result : result;
}
}
return 0;
}
});
}
/**
* Makes sure that the object specified by path, relative to container, is fetched.
*/
private static Task<Void> fetchIncludeAsync(
final OfflineStore store,
final Object container,
final String path,
final ParseSQLiteDatabase db)
throws ParseException {
// If there's no object to include, that's fine.
if (container == null) {
return Task.forResult(null);
}
// If the container is a list or array, fetch all the sub-items.
if (container instanceof Collection) {
Collection<?> collection = (Collection<?>) container;
// We do the fetches in series because it makes it easier to fail on the first error.
Task<Void> task = Task.forResult(null);
for (final Object item : collection) {
task = task.onSuccessTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
return fetchIncludeAsync(store, item, path, db);
}
});
}
return task;
} else if (container instanceof JSONArray) {
final JSONArray array = (JSONArray) container;
// We do the fetches in series because it makes it easier to fail on the first error.
Task<Void> task = Task.forResult(null);
for (int i = 0; i < array.length(); ++i) {
final int index = i;
task = task.onSuccessTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
return fetchIncludeAsync(store, array.get(index), path, db);
}
});
}
return task;
}
// If we've reached the end of the path, then actually do the fetch.
if (path == null) {
if (JSONObject.NULL.equals(container)) {
// Accept JSONObject.NULL value in included field. We swallow it silently instead of
// throwing an exception.
return Task.forResult(null);
} else if (container instanceof ParseObject) {
ParseObject object = (ParseObject) container;
return store.fetchLocallyAsync(object, db).makeVoid();
} else {
return Task.forError(new ParseException(
ParseException.INVALID_NESTED_KEY, "include is invalid for non-ParseObjects"));
}
}
// Descend into the container and try again.
String[] parts = path.split("\\.", 2);
final String key = parts[0];
final String rest = (parts.length > 1 ? parts[1] : null);
// Make sure the container is fetched.
return Task.<Void> forResult(null).continueWithTask(new Continuation<Void, Task<Object>>() {
@Override
public Task<Object> then(Task<Void> task) throws Exception {
if (container instanceof ParseObject) {
// Make sure this object is fetched before descending into it.
return fetchIncludeAsync(store, container, null, db).onSuccess(new Continuation<Void, Object>() {
@Override
public Object then(Task<Void> task) throws Exception {
return ((ParseObject) container).get(key);
}
});
} else if (container instanceof Map) {
return Task.forResult(((Map) container).get(key));
} else if (container instanceof JSONObject) {
return Task.forResult(((JSONObject) container).opt(key));
} else if (JSONObject.NULL.equals(container)) {
// Accept JSONObject.NULL value in included field. We swallow it silently instead of
// throwing an exception.
return null;
} else {
return Task.forError(new IllegalStateException("include is invalid"));
}
}
}).onSuccessTask(new Continuation<Object, Task<Void>>() {
@Override
public Task<Void> then(Task<Object> task) throws Exception {
return fetchIncludeAsync(store, task.getResult(), rest, db);
}
});
}
/**
* Makes sure all of the objects included by the given query get fetched.
*/
/* package */ static <T extends ParseObject> Task<Void> fetchIncludesAsync(
final OfflineStore store,
final T object,
ParseQuery.State<T> state,
final ParseSQLiteDatabase db) {
Set<String> includes = state.includes();
// We do the fetches in series because it makes it easier to fail on the first error.
Task<Void> task = Task.forResult(null);
for (final String include : includes) {
task = task.onSuccessTask(new Continuation<Void, Task<Void>>() {
@Override
public Task<Void> then(Task<Void> task) throws Exception {
return fetchIncludeAsync(store, object, include, db);
}
});
}
return task;
}
}