services/cloudsync/CloudSyncPlacesWrapper.jsm
author Eric Rahm <erahm@mozilla.com>
Thu, 10 Nov 2016 12:47:23 -0800
changeset 348770 e27050b02d69b88e19e0dcd151916f78259f9bb1
parent 293037 1de5ecadcceebb00801baeddabbb76f140a85d66
child 357216 7e0a0bd74199817012e200693a989ef47c999102
permissions -rw-r--r--
Bug 1313488 - Part 1: Convert XPCOM test TestDeadlockDetector to a gtest. r=froydnj This converts TestDeadlockDetector to a gtest. The logic for spawning off subprocesses is replaced with gtest's built-in death tests. On linux this will clone() the process and assert that the child process generates the appropriate assertion message. On OSX it will use fork(). In theory this should work on Windows as well buy spawning a new process but this test currently disabled there. MozReview-Commit-ID: 9Sl0hHBVGT3

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

this.EXPORTED_SYMBOLS = ["PlacesWrapper"];

const {interfaces: Ci, utils: Cu} = Components;
const REASON_ERROR = Ci.mozIStorageStatementCallback.REASON_ERROR;

Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource:///modules/PlacesUIUtils.jsm");
Cu.import("resource://services-common/utils.js");

var PlacesQueries = function () {
}

PlacesQueries.prototype = {
  cachedStmts: {},

  getQuery: function (queryString) {
    if (queryString in this.cachedStmts) {
      return this.cachedStmts[queryString];
    }

    let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
    return this.cachedStmts[queryString] = db.createAsyncStatement(queryString);
  }
};

var PlacesWrapper = function () {
}

PlacesWrapper.prototype = {
  placesQueries: new PlacesQueries(),

  guidToLocalId: function (guid) {
    let deferred = Promise.defer();

    let stmt = "SELECT id AS item_id " +
               "FROM moz_bookmarks " +
               "WHERE guid = :guid";
    let query = this.placesQueries.getQuery(stmt);

    function getLocalId(results) {
      let result = results[0] && results[0]["item_id"];
      return Promise.resolve(result);
    }

    query.params.guid = guid.toString();

    this.asyncQuery(query, ["item_id"])
        .then(getLocalId, deferred.reject)
        .then(deferred.resolve, deferred.reject);

    return deferred.promise;
  },

  localIdToGuid: function (id) {
    let deferred = Promise.defer();

    let stmt = "SELECT guid " +
               "FROM moz_bookmarks " +
               "WHERE id = :item_id";
    let query = this.placesQueries.getQuery(stmt);

    function getGuid(results) {
      let result = results[0] && results[0]["guid"];
      return Promise.resolve(result);
    }

    query.params.item_id = id;

    this.asyncQuery(query, ["guid"])
        .then(getGuid, deferred.reject)
        .then(deferred.resolve, deferred.reject);

    return deferred.promise;
  },

  getItemsById: function (ids, types) {
    let deferred = Promise.defer();
    let stmt = "SELECT b.id, b.type, b.parent, b.position, b.title, b.guid, b.dateAdded, b.lastModified, p.url " +
               "FROM moz_bookmarks b " +
               "LEFT JOIN moz_places p ON b.fk = p.id " +
               "WHERE b.id in (" + ids.join(",") + ") AND b.type in (" + types.join(",") + ")";
    let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
    let query = db.createAsyncStatement(stmt);

    this.asyncQuery(query, ["id", "type", "parent", "position", "title", "guid", "dateAdded", "lastModified", "url"])
        .then(deferred.resolve, deferred.reject);

    return deferred.promise;
  },

  getItemsByParentId: function (parents, types) {
    let deferred = Promise.defer();
    let stmt = "SELECT b.id, b.type, b.parent, b.position, b.title, b.guid, b.dateAdded, b.lastModified, p.url " +
               "FROM moz_bookmarks b " +
               "LEFT JOIN moz_places p ON b.fk = p.id " +
               "WHERE b.parent in (" + parents.join(",") + ") AND b.type in (" + types.join(",") + ")";
    let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
    let query = db.createAsyncStatement(stmt);

    this.asyncQuery(query, ["id", "type", "parent", "position", "title", "guid", "dateAdded", "lastModified", "url"])
        .then(deferred.resolve, deferred.reject);

    return deferred.promise;
  },

  getItemsByGuid: function (guids, types) {
    let deferred = Promise.defer();
    guids = guids.map(JSON.stringify);
    let stmt = "SELECT b.id, b.type, b.parent, b.position, b.title, b.guid, b.dateAdded, b.lastModified, p.url " +
               "FROM moz_bookmarks b " +
               "LEFT JOIN moz_places p ON b.fk = p.id " +
               "WHERE b.guid in (" + guids.join(",") + ") AND b.type in (" + types.join(",") + ")";
    let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
    let query = db.createAsyncStatement(stmt);

    this.asyncQuery(query, ["id", "type", "parent", "position", "title", "guid", "dateAdded", "lastModified", "url"])
        .then(deferred.resolve, deferred.reject);

    return deferred.promise;
  },

  updateCachedFolderIds: function (folderCache, folder) {
    let deferred = Promise.defer();
    let stmt = "SELECT id, guid " +
               "FROM moz_bookmarks " +
               "WHERE parent = :parent_id AND type = :item_type";
    let query = this.placesQueries.getQuery(stmt);

    query.params.parent_id = folder;
    query.params.item_type = PlacesUtils.bookmarks.TYPE_FOLDER;

    this.asyncQuery(query, ["id", "guid"]).then(
      function (items) {
        let previousIds = folderCache.getChildren(folder);
        let currentIds = new Set();
        for (let item of items) {
          currentIds.add(item.id);
        }
        let newIds = new Set();
        let missingIds = new Set();

        for (let currentId of currentIds) {
          if (!previousIds.has(currentId)) {
            newIds.add(currentId);
          }
        }
        for (let previousId of previousIds) {
          if (!currentIds.has(previousId)) {
            missingIds.add(previousId);
          }
        }

        folderCache.setChildren(folder, currentIds);

        let promises = [];
        for (let newId of newIds) {
          promises.push(this.updateCachedFolderIds(folderCache, newId));
        }
        Promise.all(promises)
               .then(deferred.resolve, deferred.reject);

        for (let missingId of missingIds) {
          folderCache.remove(missingId);
        }
      }.bind(this)
    );

    return deferred.promise;
  },

  getLocalIdsWithAnnotation: function (anno) {
    let deferred = Promise.defer();
    let stmt = "SELECT a.item_id " +
               "FROM moz_anno_attributes n " +
               "JOIN moz_items_annos a ON n.id = a.anno_attribute_id " +
               "WHERE n.name = :anno_name";
    let query = this.placesQueries.getQuery(stmt);

    query.params.anno_name = anno.toString();

    this.asyncQuery(query, ["item_id"])
        .then(function (items) {
                let results = [];
                for (let item of items) {
                  results.push(item.item_id);
                }
                deferred.resolve(results);
              },
              deferred.reject);

    return deferred.promise;
  },

  getItemAnnotationsForLocalId: function (id) {
    let deferred = Promise.defer();
    let stmt = "SELECT a.name, b.content " +
               "FROM moz_anno_attributes a " +
               "JOIN moz_items_annos b ON a.id = b.anno_attribute_id " +
               "WHERE b.item_id = :item_id";
    let query = this.placesQueries.getQuery(stmt);

    query.params.item_id = id;

    this.asyncQuery(query, ["name", "content"])
        .then(function (results) {
                let annos = {};
                for (let result of results) {
                  annos[result.name] = result.content;
                }
                deferred.resolve(annos);
              },
              deferred.reject);

    return deferred.promise;
  },

  insertBookmark: function (parent, uri, index, title, guid) {
    let parsedURI;
    try {
      parsedURI = CommonUtils.makeURI(uri)
    } catch (e) {
      return Promise.reject("unable to parse URI '" + uri + "': " + e);
    }

    try {
      let id = PlacesUtils.bookmarks.insertBookmark(parent, parsedURI, index, title, guid);
      return Promise.resolve(id);
    } catch (e) {
      return Promise.reject("unable to insert bookmark " + JSON.stringify(arguments) + ": " + e);
    }
  },

  setItemAnnotation: function (item, anno, value, flags, exp) {
    try {
      return Promise.resolve(PlacesUtils.annotations.setItemAnnotation(item, anno, value, flags, exp));
    } catch (e) {
      return Promise.reject(e);
    }
  },

  itemHasAnnotation: function (item, anno) {
    try {
      return Promise.resolve(PlacesUtils.annotations.itemHasAnnotation(item, anno));
    } catch (e) {
      return Promise.reject(e);
    }
  },

  createFolder: function (parent, name, index, guid) {
    try {
      return Promise.resolve(PlacesUtils.bookmarks.createFolder(parent, name, index, guid));
    } catch (e) {
      return Promise.reject("unable to create folder ['" + name + "']: " + e);
    }
  },

  removeFolderChildren: function (folder) {
    try {
      PlacesUtils.bookmarks.removeFolderChildren(folder);
      return Promise.resolve();
    } catch (e) {
      return Promise.reject(e);
    }
  },

  insertSeparator: function (parent, index, guid) {
    try {
      return Promise.resolve(PlacesUtils.bookmarks.insertSeparator(parent, index, guid));
    } catch (e) {
      return Promise.reject(e);
    }
  },

  removeItem: function (item) {
    try {
      return Promise.resolve(PlacesUtils.bookmarks.removeItem(item));
    } catch (e) {
      return Promise.reject(e);
    }
  },

  setItemDateAdded: function (item, dateAdded) {
    try {
      return Promise.resolve(PlacesUtils.bookmarks.setItemDateAdded(item, dateAdded));
    } catch (e) {
      return Promise.reject(e);
    }
  },

  setItemLastModified: function (item, lastModified) {
    try {
      return Promise.resolve(PlacesUtils.bookmarks.setItemLastModified(item, lastModified));
    } catch (e) {
      return Promise.reject(e);
    }
  },

  setItemTitle: function (item, title) {
    try {
      return Promise.resolve(PlacesUtils.bookmarks.setItemTitle(item, title));
    } catch (e) {
      return Promise.reject(e);
    }
  },

  changeBookmarkURI: function (item, uri) {
    try {
      uri = CommonUtils.makeURI(uri);
      return Promise.resolve(PlacesUtils.bookmarks.changeBookmarkURI(item, uri));
    } catch (e) {
      return Promise.reject(e);
    }
  },

  moveItem: function (item, parent, index) {
    try {
      return Promise.resolve(PlacesUtils.bookmarks.moveItem(item, parent, index));
    } catch (e) {
      return Promise.reject(e);
    }
  },

  setItemIndex: function (item, index) {
    try {
      return Promise.resolve(PlacesUtils.bookmarks.setItemIndex(item, index));
    } catch (e) {
      return Promise.reject(e);
    }
  },

  asyncQuery: function (query, names) {
    let deferred = Promise.defer();
    let storageCallback = {
      results: [],
      handleResult: function (results) {
        if (!names) {
          return;
        }

        let row;
        while ((row = results.getNextRow()) != null) {
          let item = {};
          for (let name of names) {
            item[name] = row.getResultByName(name);
          }
          this.results.push(item);
        }
      },

      handleError: function (error) {
        deferred.reject(error);
      },

      handleCompletion: function (reason) {
        if (REASON_ERROR == reason) {
          return;
        }

        deferred.resolve(this.results);
      }
    };

    query.executeAsync(storageCallback);
    return deferred.promise;
  },
};

this.PlacesWrapper = new PlacesWrapper();