com.parse.OfflineQueryLogic.java Source code

Java tutorial

Introduction

Here is the source code for com.parse.OfflineQueryLogic.java

Source

/*
 * 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();
        }

        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());
    }

    /**
     * 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);

        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;
    }
}