All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.parse.OfflineQueryLogic Maven / Gradle / Ivy

Go to download

A library that gives you access to the powerful Parse cloud platform from your Android app.

There is a newer version: 1.17.3
Show newest version
/*
 * 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 {

    /* package */ final ParseUser user;

    public ConstraintMatcher(ParseUser user) {
      this.user = user;
    }

    /* package */ abstract Task 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> constraintMap =
        (HashMap>) constraint;
    ArrayList 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 extends ConstraintMatcher {
    private final ParseQuery.State subQuery;
    private Task> subQueryResults = null;

    public SubQueryMatcher(ParseUser user, ParseQuery.State subQuery) {
      super(user);
      this.subQuery = subQuery;
    }

    @Override
    public Task 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, Boolean>() {
        @Override
        public Boolean then(Task> task) throws ParseException {
          return matches(object, task.getResult());
        }
      });
    }

    protected abstract boolean matches(T object, List results) throws ParseException;
  }

  /**
   * Creates a matcher that handles $inQuery constraints.
   */
  private  ConstraintMatcher createInQueryMatcher(ParseUser user,
      Object constraint, final String key) {
    // TODO(grantland): Convert builder to state t6941155
    @SuppressWarnings("unchecked")
    ParseQuery.State query = ((ParseQuery.State.Builder) constraint).build();
    return new SubQueryMatcher(user, query) {
      @Override
      protected boolean matches(T object, List results) throws ParseException {
        Object value = getValue(object, key);
        return matchesInConstraint(results, value);
      }
    };
  }

  /**
   * Creates a matcher that handles $notInQuery constraints.
   */
  private  ConstraintMatcher createNotInQueryMatcher(ParseUser user,
      Object constraint, final String key) {
    final ConstraintMatcher inQueryMatcher = createInQueryMatcher(user, constraint, key);
    return new ConstraintMatcher(user) {
      @Override
      public Task matchesAsync(T object, ParseSQLiteDatabase db) {
        return inQueryMatcher.matchesAsync(object, db).onSuccess(new Continuation() {
          @Override
          public Boolean then(Task task) throws Exception {
            return !task.getResult();
          }
        });
      }
    };
  }

  /**
   * Creates a matcher that handles $select constraints.
   */
  private  ConstraintMatcher createSelectMatcher(ParseUser user,
      Object constraint, final String key) {
    Map constraintMap = (Map) constraint;
    // TODO(grantland): Convert builder to state t6941155
    @SuppressWarnings("unchecked")
    ParseQuery.State query = ((ParseQuery.State.Builder) constraintMap.get("query")).build();
    final String resultKey = (String) constraintMap.get("key");
    return new SubQueryMatcher(user, query) {
      @Override
      protected boolean matches(T object, List 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  ConstraintMatcher createDontSelectMatcher(ParseUser user,
      Object constraint, final String key) {
    final ConstraintMatcher selectMatcher = createSelectMatcher(user, constraint, key);
    return new ConstraintMatcher(user) {
      @Override
      public Task matchesAsync(T object, ParseSQLiteDatabase db) {
        return selectMatcher.matchesAsync(object, db).onSuccess(new Continuation() {
          @Override
          public Boolean then(Task task) throws Exception {
            return !task.getResult();
          }
        });
      }
    };
  }

  /*
   * Creates a matcher for a particular constraint operator.
   */
  private  ConstraintMatcher 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(user) {
          @Override
          public Task 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  ConstraintMatcher createOrMatcher(ParseUser user,
      ArrayList queries) {
    // Make a list of all the matchers to OR together.
    final ArrayList> matchers = new ArrayList<>();
    for (QueryConstraints constraints : queries) {
      ConstraintMatcher matcher = createMatcher(user, constraints);
      matchers.add(matcher);
    }
    /*
     * Now OR together the constraints for each query.
     */
    return new ConstraintMatcher(user) {
      @Override
      public Task matchesAsync(final T object, final ParseSQLiteDatabase db) {
        Task task = Task.forResult(false);
        for (final ConstraintMatcher matcher : matchers) {
          task = task.onSuccessTask(new Continuation>() {
            @Override
            public Task then(Task 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  ConstraintMatcher createMatcher(ParseUser user,
      QueryConstraints queryConstraints) {
    // Make a list of all the matchers to AND together.
    final ArrayList> 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 matcher =
            createOrMatcher(user, (ArrayList) 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 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(user) {
          @Override
          public Task 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(user) {
          @Override
          public Task 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(user) {
      @Override
      public Task matchesAsync(final T object, final ParseSQLiteDatabase db) {
        Task task = Task.forResult(true);
        for (final ConstraintMatcher matcher : matchers) {
          task = task.onSuccessTask(new Continuation>() {
            @Override
            public Task then(Task 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  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  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  Subclass of ParseObject.
   * @return A new instance of ConstraintMatcher.
   */
  /* package */  ConstraintMatcher createMatcher(
      ParseQuery.State state, final ParseUser user) {
    final boolean ignoreACLs = state.ignoreACLs();
    final ConstraintMatcher constraintMatcher = createMatcher(user, state.constraints());

    return new ConstraintMatcher(user) {
      @Override
      public Task 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  void sort(List results, ParseQuery.State state)
      throws ParseException {
    final List 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() {
      @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 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 task = Task.forResult(null);
      for (final Object item : collection) {
        task = task.onSuccessTask(new Continuation>() {
          @Override
          public Task then(Task 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 task = Task.forResult(null);
      for (int i = 0; i < array.length(); ++i) {
        final int index = i;
        task = task.onSuccessTask(new Continuation>() {
          @Override
          public Task then(Task 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. forResult(null).continueWithTask(new Continuation>() {
      @Override
      public Task then(Task 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() {
            @Override
            public Object then(Task 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>() {
      @Override
      public Task then(Task 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  Task fetchIncludesAsync(
      final OfflineStore store,
      final T object,
      ParseQuery.State state,
      final ParseSQLiteDatabase db) {
    Set includes = state.includes();
    // We do the fetches in series because it makes it easier to fail on the first error.
    Task task = Task.forResult(null);
    for (final String include : includes) {
      task = task.onSuccessTask(new Continuation>() {
        @Override
        public Task then(Task task) throws Exception {
          return fetchIncludeAsync(store, object, include, db);
        }
      });
    }
    return task;
  }
}