зеркало из https://github.com/mozilla/snowl.git
1837 строки
66 KiB
JavaScript
1837 строки
66 KiB
JavaScript
/* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is Snowl.
|
|
*
|
|
* The Initial Developer of the Original Code is Mozilla.
|
|
* Portions created by the Initial Developer are Copyright (C) 2008
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Myk Melez <myk@mozilla.org>
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
let EXPORTED_SYMBOLS = ["SnowlDatastore", "SnowlPlaces", "SnowlQuery"];
|
|
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
const Cr = Components.results;
|
|
const Cu = Components.utils;
|
|
|
|
// modules that come with Firefox
|
|
Cu.import("resource://gre/modules/utils.js"); // Places
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
// modules that are generic
|
|
Cu.import("resource://snowl/modules/log4moz.js");
|
|
Cu.import("resource://snowl/modules/Observers.js");
|
|
Cu.import("resource://snowl/modules/StringBundle.js");
|
|
Cu.import("resource://snowl/modules/URI.js");
|
|
|
|
// modules that are Snowl-specific
|
|
Cu.import("resource://snowl/modules/constants.js");
|
|
|
|
let strings = new StringBundle("chrome://snowl/locale/datastore.properties");
|
|
|
|
const TABLE_TYPE_NORMAL = 0;
|
|
const TABLE_TYPE_FULLTEXT = 1;
|
|
|
|
let SnowlDatastore = {
|
|
get _storage() {
|
|
delete this._storage;
|
|
return this._storage = Cc["@mozilla.org/storage/service;1"].
|
|
getService(Ci.mozIStorageService);
|
|
},
|
|
|
|
get _log() {
|
|
delete this._log;
|
|
return this._log = Log4Moz.repository.getLogger("Snowl.Datastore");
|
|
},
|
|
|
|
//**************************************************************************//
|
|
// Database Creation & Access
|
|
|
|
_dbVersion: 14,
|
|
_dbFileIsNew: false,
|
|
|
|
_dbSchema: {
|
|
// Note: datetime values like messages:timestamp are stored as Julian dates.
|
|
|
|
// FIXME: establish a universalID property of messages that is unique
|
|
// across sources for cases in which multiple sources provide the same
|
|
// message, they identify it via an ID that is unique across sources,
|
|
// and we don't want to duplicate the message.
|
|
|
|
// FIXME: make the datastore support multiple authors.
|
|
|
|
// FIXME: support labeling the subject as HTML or another content type.
|
|
|
|
// FIXME: define TABLE_TYPE_FULLTEXT tables in a separate fulltextTables
|
|
// property and create them via a separate _dbCreateFulltextTables function
|
|
// just as we define indexes in a separate indexes property and create them
|
|
// via a separate _dbCreateIndexes function.
|
|
|
|
// FIXME: make the tables property an array of tables just as the indexes
|
|
// property is an array of indexes so we can create them in a specific order
|
|
// that respects their (currently only advisory) foreign key constraints.
|
|
|
|
// FIXME: make columns be objects with name, type, & constraint properties.
|
|
|
|
tables: {
|
|
sources: {
|
|
type: TABLE_TYPE_NORMAL,
|
|
columns: [
|
|
"id INTEGER PRIMARY KEY",
|
|
"type TEXT NOT NULL",
|
|
"name TEXT NOT NULL",
|
|
// XXX Call these URL instead of URI, since they are unambiguously
|
|
// locations, not names (and thus never URNs)?
|
|
"machineURI TEXT NOT NULL",
|
|
"humanURI TEXT",
|
|
"username TEXT",
|
|
"lastRefreshed REAL",
|
|
"importance INTEGER",
|
|
"placeID INTEGER",
|
|
// JSON object string.
|
|
"attributes TEXT DEFAULT '{}'"
|
|
]
|
|
},
|
|
|
|
messages: {
|
|
type: TABLE_TYPE_NORMAL,
|
|
columns: [
|
|
"id INTEGER PRIMARY KEY",
|
|
"sourceID INTEGER NOT NULL REFERENCES sources(id)",
|
|
|
|
// externalID is a unique identifier provided by the source
|
|
// of the message which is constant across message transfer points
|
|
// and destinations. For feeds, it's the entry ID; for email,
|
|
// it's the message ID; for tweets, it's the tweet ID.
|
|
//
|
|
// externalID is a BLOB because some sources (like Twitter) give
|
|
// messages integer IDs, and if it were TEXT, then we'd have to
|
|
// CAST(externalID AS INTEGER) to get it as an integer in order to
|
|
// do things like get the MAX(externalID) for a given Twitter source
|
|
// so we can retrieve only messages since_id=<the max ID>.
|
|
"externalID BLOB",
|
|
|
|
"subject TEXT",
|
|
"authorID INTEGER REFERENCES people(id)",
|
|
|
|
// timestamp represents the date/time assigned to the message by its
|
|
// source. It can have multiple meanings, including when the message
|
|
// was "sent" by its author, when it was published, and when it was
|
|
// last updated.
|
|
"timestamp REAL",
|
|
|
|
// received represents the date/time at which the message was first
|
|
// received by this application.
|
|
"received REAL",
|
|
|
|
"link TEXT",
|
|
"current INTEGER DEFAULT 1",
|
|
"read INTEGER DEFAULT 0",
|
|
// JSON object string.
|
|
"headers TEXT DEFAULT '{}'",
|
|
// JSON object string.
|
|
"attributes TEXT DEFAULT '{}'"
|
|
]
|
|
},
|
|
|
|
parts: {
|
|
type: TABLE_TYPE_NORMAL,
|
|
columns: [
|
|
"id INTEGER PRIMARY KEY",
|
|
"messageID INTEGER NOT NULL REFERENCES messages(id)",
|
|
"content NOT NULL",
|
|
// The DEFAULT constraint helps when upgrading from schemas
|
|
// that didn't require mediaType to be NOT NULL, so it might
|
|
// have contained NULL values.
|
|
"mediaType TEXT NOT NULL DEFAULT 'application/octet-stream'",
|
|
"partType INTEGER NOT NULL",
|
|
"baseURI TEXT",
|
|
"languageTag TEXT"
|
|
]
|
|
},
|
|
|
|
partsText: {
|
|
type: TABLE_TYPE_FULLTEXT,
|
|
columns: [
|
|
// partsText has an implicit docid column whose value we set to the ID
|
|
// of the corresponding record in the parts table so we can join them
|
|
// to get the part (and thence message) for a fulltext search result.
|
|
"content"
|
|
]
|
|
},
|
|
|
|
people: {
|
|
type: TABLE_TYPE_NORMAL,
|
|
columns: [
|
|
"id INTEGER PRIMARY KEY",
|
|
// XXX Should we store this info as part of the identity, so that
|
|
// a person with multiple identities could retain information from
|
|
// all of them and select from it at display time?
|
|
"name TEXT NOT NULL",
|
|
"homeURL TEXT",
|
|
"iconURL TEXT",
|
|
"placeID INTEGER"
|
|
]
|
|
},
|
|
|
|
identities: {
|
|
type: TABLE_TYPE_NORMAL,
|
|
columns: [
|
|
"id INTEGER PRIMARY KEY",
|
|
"sourceID INTEGER NOT NULL REFERENCES sources(id)",
|
|
"externalID TEXT NOT NULL",
|
|
"personID INTEGER NOT NULL REFERENCES people(id)",
|
|
"UNIQUE(externalID, sourceID)"
|
|
]
|
|
},
|
|
|
|
collections: {
|
|
type: TABLE_TYPE_NORMAL,
|
|
columns: [
|
|
"id INTEGER PRIMARY KEY",
|
|
"name TEXT NOT NULL",
|
|
"iconURL TEXT",
|
|
"orderKey INTEGER NOT NULL DEFAULT 0",
|
|
"grouped BOOLEAN DEFAULT 0",
|
|
"groupIDColumn TEXT",
|
|
"groupNameColumn TEXT",
|
|
"groupHomeURLColumn TEXT",
|
|
"groupIconURLColumn TEXT"
|
|
]
|
|
}
|
|
},
|
|
|
|
indexes: [
|
|
// Index the sourceID and externalID columns in the messages table
|
|
// to speed up checking if a message we're receiving from a source
|
|
// is already in the datastore.
|
|
{
|
|
name: "messages_sourceID_externalID",
|
|
table: "messages",
|
|
columns: ["sourceID", "externalID"]
|
|
},
|
|
|
|
// Index the messageID column in the parts table to speed up retrieval
|
|
// of content for a specific message, which we do a lot.
|
|
{
|
|
name: "parts_messageID",
|
|
table: "parts",
|
|
columns: ["messageID"]
|
|
}
|
|
]
|
|
|
|
},
|
|
|
|
_defaultCollections: [
|
|
{ name: strings.get("allCollectionName"),
|
|
iconURL: "chrome://snowl/content/icons/rainbow.png",
|
|
orderKey: 1,
|
|
grouped: false },
|
|
|
|
{ name: strings.get("sourcesCollectionName"),
|
|
iconURL: "chrome://snowl/skin/livemarkFolder-16.png",
|
|
orderKey: 2,
|
|
grouped: true,
|
|
groupIDColumn: "sources.id",
|
|
groupNameColumn: "sources.name",
|
|
groupHomeURLColumn: "sources.humanURI" },
|
|
|
|
{ name: strings.get("authorsCollectionName"),
|
|
iconURL: "chrome://snowl/skin/person-16.png",
|
|
orderKey: 3,
|
|
grouped: true,
|
|
groupIDColumn: "authors.id",
|
|
groupNameColumn: "authors.name",
|
|
groupIconURLColumn: "authors.iconURL" }
|
|
],
|
|
|
|
dbConnection: null,
|
|
|
|
// Statements that are created via the createStatement method. We use this
|
|
// to finalize statements when finalizeStatements is called (so we can close
|
|
// the connection).
|
|
_statements: [],
|
|
|
|
createStatement: function(aSQLString, aDBConnection) {
|
|
let dbConnection = aDBConnection ? aDBConnection : this.dbConnection;
|
|
let wrappedStatement;
|
|
|
|
try {
|
|
let statement = dbConnection.createStatement(aSQLString);
|
|
wrappedStatement = new InstrumentedStorageStatement(aSQLString, statement);
|
|
}
|
|
catch(ex) {
|
|
throw("error creating statement " + aSQLString + " - " +
|
|
dbConnection.lastError + ": " +
|
|
dbConnection.lastErrorString + " - " + ex);
|
|
}
|
|
|
|
this._statements.push(wrappedStatement);
|
|
return wrappedStatement;
|
|
},
|
|
|
|
finalizeStatements: function() {
|
|
for each (statement in this._statements) {
|
|
if (statement instanceof InstrumentedStorageStatement)
|
|
statement = statement._statement;
|
|
if (statement instanceof Ci.mozIStorageStatementWrapper)
|
|
statement = statement.statement;
|
|
if (statement instanceof Ci.mozIStorageStatement)
|
|
statement.finalize();
|
|
else
|
|
this._log.warning("can't finalize " + statement);
|
|
}
|
|
},
|
|
|
|
// _dbInit, the methods it calls (_dbCreateTables, _dbMigrate), and methods
|
|
// those methods call must be careful not to call any method of the service
|
|
// that assumes the database connection has already been initialized,
|
|
// since it won't be initialized until this function returns.
|
|
|
|
_dbInit: function() {
|
|
var dirService = Cc["@mozilla.org/file/directory_service;1"].
|
|
getService(Ci.nsIProperties);
|
|
var dbFile = dirService.get("ProfD", Ci.nsIFile);
|
|
dbFile.append("messages.sqlite");
|
|
|
|
var dbService = Cc["@mozilla.org/storage/service;1"].
|
|
getService(Ci.mozIStorageService);
|
|
|
|
var dbConnection;
|
|
|
|
if (!dbFile.exists()) {
|
|
dbConnection = dbService.openUnsharedDatabase(dbFile);
|
|
this._dbCreate(dbConnection);
|
|
}
|
|
else {
|
|
try {
|
|
dbConnection = dbService.openUnsharedDatabase(dbFile);
|
|
|
|
// Get the version of the database in the file.
|
|
var version = dbConnection.schemaVersion;
|
|
|
|
if (version != this._dbVersion)
|
|
this._dbMigrate(dbConnection, version, this._dbVersion);
|
|
}
|
|
catch (ex) {
|
|
// If the database file is corrupted, I'm not sure whether we should
|
|
// just delete the corrupted file or back it up. For now I'm just
|
|
// deleting it, but here's some code that backs it up (but doesn't limit
|
|
// the number of backups, which is probably necessary, thus I'm not
|
|
// using this code):
|
|
//var backup = this._dbFile.clone();
|
|
//backup.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
|
|
//backup.remove(false);
|
|
//this._dbFile.moveTo(null, backup.leafName);
|
|
if (ex.result == Cr.NS_ERROR_FILE_CORRUPTED) {
|
|
// Remove the corrupted file, then recreate it.
|
|
dbFile.remove(false);
|
|
dbConnection = dbService.openUnsharedDatabase(dbFile);
|
|
this._dbCreate(dbConnection);
|
|
}
|
|
else
|
|
throw ex;
|
|
}
|
|
}
|
|
|
|
this.dbConnection = dbConnection;
|
|
|
|
// Register sqlite regex function.
|
|
this.dbConnection.createFunction("REGEXP", 2, this._dbRegexp);
|
|
},
|
|
|
|
_dbCreate: function(dbConnection) {
|
|
dbConnection.beginTransaction();
|
|
try {
|
|
this._dbCreateTables(dbConnection);
|
|
this._dbCreateIndexes(dbConnection);
|
|
dbConnection.schemaVersion = this._dbVersion;
|
|
this._dbInsertDefaultData(dbConnection);
|
|
dbConnection.commitTransaction();
|
|
this._dbFileIsNew = true;
|
|
}
|
|
catch(ex) {
|
|
dbConnection.rollbackTransaction();
|
|
throw ex;
|
|
}
|
|
},
|
|
|
|
_dbCreateTables: function(aDBConnection) {
|
|
for (let tableName in this._dbSchema.tables) {
|
|
let table = this._dbSchema.tables[tableName];
|
|
this._dbCreateTable(aDBConnection, tableName, table);
|
|
}
|
|
},
|
|
|
|
_dbCreateTable: function(aDBConnection, tableName, table) {
|
|
switch (table.type) {
|
|
case TABLE_TYPE_FULLTEXT:
|
|
aDBConnection.executeSimpleSQL(
|
|
"CREATE VIRTUAL TABLE " + tableName +
|
|
" USING fts3(" + table.columns.join(", ") + ")"
|
|
);
|
|
break;
|
|
|
|
case TABLE_TYPE_NORMAL:
|
|
default:
|
|
aDBConnection.createTable(tableName, table.columns.join(", "));
|
|
break;
|
|
}
|
|
},
|
|
|
|
_dbCreateIndexes: function(dbConnection) {
|
|
for each (let index in this._dbSchema.indexes)
|
|
this._dbCreateIndex(dbConnection, index);
|
|
},
|
|
|
|
_dbCreateIndex: function(dbConnection, index) {
|
|
dbConnection.executeSimpleSQL(
|
|
"CREATE INDEX " + index.name + " ON " + index.table +
|
|
"(" + index.columns.join(", ") + ")"
|
|
);
|
|
},
|
|
|
|
_dbDropIndex: function(dbConnection, index) {
|
|
dbConnection.executeSimpleSQL(
|
|
"DROP INDEX " + index.name
|
|
);
|
|
},
|
|
|
|
|
|
_dbInsertDefaultData: function(aDBConnection) {
|
|
let params = ["name", "iconURL", "orderKey", "grouped", "groupIDColumn",
|
|
"groupNameColumn", "groupHomeURLColumn",
|
|
"groupIconURLColumn"];
|
|
|
|
let statement = this.createStatement(
|
|
"INSERT INTO collections (" + params.join(", ") + ") " +
|
|
"VALUES (" + params.map(function(v) ":" + v).join(", ") + ")",
|
|
aDBConnection);
|
|
|
|
for each (let collection in this._defaultCollections) {
|
|
for each (let param in params)
|
|
statement.params[param] = (param in collection) ? collection[param] : null;
|
|
statement.execute();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Define regexp test function for sqlite. This method creates a global
|
|
* regexp object to be reused by sqlite. Getter names are used as tokens
|
|
* in the sqlite statement and any |test| expression can be added.
|
|
* XXX: Using executeAsync will cause Fx to freeze after a few runs of a
|
|
* statement with a regexp function.
|
|
*
|
|
* Usage:
|
|
* (0) = Name of string passed from the sqlite query, for a tokenized regexp.
|
|
* For a dynamic regex, the string is first set by calling regExp(val),
|
|
* then executing the sqlite query with the dynamic function token.
|
|
* (1) = Column value to test.
|
|
* Statement eg, "WHERE attributes REGEXP 'FLAGGED_TRUE'"
|
|
*
|
|
* @return {Boolean} indicates if regex test successful; record returned if true.
|
|
*/
|
|
_dbRegexp: {
|
|
// Valid tokens for the sql regexp expression for test.
|
|
get ValidTokens() {
|
|
if (!this._ValidTokens)
|
|
this._ValidTokens = {
|
|
"FLAGGED_TRUE" : /"flagged":true/g
|
|
};
|
|
return this._ValidTokens;
|
|
},
|
|
|
|
// Valid parms for the sql regexp expression and preprocessing re for match.
|
|
get ValidParms() {
|
|
if (!this._ValidParms)
|
|
this._ValidParms = {
|
|
"SUBJECT" : null,
|
|
"SENDER" : null,
|
|
"MESSAGES" : null,
|
|
// Remove header names, of format |"name":| - only search values.
|
|
"HEADERS" : /[{,]\"[^\"]*\":/g
|
|
};
|
|
return this._ValidParms;
|
|
},
|
|
|
|
get termsArray() {
|
|
return this._termsArray;
|
|
},
|
|
|
|
set termsArray(newVal) {
|
|
this._termsArray = newVal;
|
|
},
|
|
|
|
get ignoreCase() {
|
|
return this._ignoreCase;
|
|
},
|
|
|
|
set ignoreCase(newVal) {
|
|
this._ignoreCase = newVal ? true : false;
|
|
},
|
|
|
|
get headerToSearchRE() {
|
|
return this._headerToSearchRE;
|
|
},
|
|
|
|
set headerToSearchRE(newStr) {
|
|
this._headerToSearchRE = newStr;
|
|
},
|
|
|
|
// Dynamic regexp for headers using the search string placed in termsArray.
|
|
RegexMatch: function(aStr, aParm) {
|
|
//SnowlDatastore._log.info("RegexMatch: START:aStr - "); //+" : "+str);
|
|
let str;
|
|
let match = true;
|
|
let matchOr = false;
|
|
let matchNot = false;
|
|
let flags = "g" + (this.ignoreCase ? "i" : "");
|
|
|
|
str = this.ValidParms[aParm] ? aStr.replace(this.ValidParms[aParm], "") :
|
|
aStr;
|
|
str = str == null ? "" : str;
|
|
//SnowlDatastore._log.info("RegexMatch: str - " +str);
|
|
|
|
for each(term in this.termsArray) {
|
|
//SnowlDatastore._log.info("RegexMatch: term:match:matchOr:matchNot - " +
|
|
// term+" : "+match+" : " +matchOr+" : " +matchNot);
|
|
if (/^OR?/.test(term)) {
|
|
matchOr = match || matchOr;
|
|
match = true;
|
|
continue;
|
|
}
|
|
|
|
if (term[0] == "-") {
|
|
if (str.match(term.slice(1), flags))
|
|
return false;
|
|
else
|
|
continue;
|
|
}
|
|
|
|
match = (str.match(term, flags) && match ? true : false);
|
|
//SnowlDatastore._log.info("RegexMatch: match - "+match);
|
|
}
|
|
|
|
return match || matchOr;
|
|
},
|
|
|
|
onFunctionCall: function(aArgs) {
|
|
if (aArgs.getString(0) in this.ValidParms)
|
|
return this.RegexMatch(aArgs.getString(1), aArgs.getString(0));
|
|
else if (aArgs.getString(0) in this.ValidTokens)
|
|
return aArgs.getString(1).match(this.ValidTokens[aArgs.getString(0)]) ? true : false;
|
|
// For some reason, test does not return all matches..
|
|
// return (this.ValidTokens[aArgs.getString(0)]).test(aArgs.getString(1)) ? true : false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Migrate the database schema from one version to another. Calls out to
|
|
* version pair specific migrator functions below. Handles migrations from
|
|
* all older to newer versions of Snowl per this Snowl to DB version map:
|
|
* 0.1 : 4
|
|
* 0.1.1 : 4
|
|
* 0.2pre1 : 5
|
|
* 0.2pre2 : 5
|
|
* 0.2pre3 : 8
|
|
* 0.2pre3.1: 8
|
|
* ..
|
|
* 0.3 : 13
|
|
* 0.4pre1 : 14
|
|
*
|
|
* Also handles migrations from each version to the next higher one for folks
|
|
* tracking development releases or the repository. And might handle migrations
|
|
* between other version pairs on occasion as warranted.
|
|
*
|
|
* FIXME: do multi-version upgrades automatically if it's possible to get to
|
|
* the latest version via a series of steps instead of writing one-off
|
|
* functions to do the database migration for every combination of versions.
|
|
*/
|
|
_dbMigrate: function(aDBConnection, aOldVersion, aNewVersion) {
|
|
if (this["_dbMigrate" + aOldVersion + "To" + aNewVersion]) {
|
|
aDBConnection.beginTransaction();
|
|
try {
|
|
// We have to dump here because this runs before the service
|
|
// has initialized the logger.
|
|
// FIXME: initialize the logger first so we can use it here.
|
|
dump("migrating database from " + aOldVersion + " to " + aNewVersion + "\n");
|
|
let start = new Date();
|
|
this["_dbMigrate" + aOldVersion + "To" + aNewVersion](aDBConnection);
|
|
aDBConnection.schemaVersion = aNewVersion;
|
|
aDBConnection.commitTransaction();
|
|
let end = new Date();
|
|
dump("database migration took " + (end - start) + "ms" + "\n");
|
|
}
|
|
catch(ex) {
|
|
aDBConnection.rollbackTransaction();
|
|
throw ex;
|
|
}
|
|
}
|
|
else
|
|
throw("can't migrate database from v" + aOldVersion +
|
|
" to v" + aNewVersion + ": no migrator function");
|
|
},
|
|
|
|
/**
|
|
* Migrate the database schema from version 0 to the current version.
|
|
*
|
|
* We never create a database with version 0, so the database can only
|
|
* have that version if the database file was created without the schema
|
|
* being constructed (f.e. because the disk was out of space and let us
|
|
* create the file but not populate it with any data). Thus, migrating
|
|
* the database is as simple as constructing the schema from scratch.
|
|
*
|
|
* FIXME: special case the calling of this function so we don't have to
|
|
* rename it every time we increase the schema version.
|
|
*/
|
|
_dbMigrate0To14: function(dbConnection) {
|
|
this._dbCreate(dbConnection);
|
|
},
|
|
|
|
_dbMigrate4To14: function(dbConnection) {
|
|
this._dbMigrate4To5(dbConnection);
|
|
this._dbMigrate5To6(dbConnection);
|
|
this._dbMigrate6To7(dbConnection);
|
|
this._dbMigrate7To8(dbConnection);
|
|
this._dbMigrate8To9(dbConnection);
|
|
this._dbMigrate9To10(dbConnection);
|
|
this._dbMigrate10To11(dbConnection);
|
|
this._dbMigrate11To12(dbConnection);
|
|
this._dbMigrate12To13(dbConnection);
|
|
this._dbMigrate13To14(dbConnection);
|
|
},
|
|
|
|
_dbMigrate5To14: function(dbConnection) {
|
|
this._dbMigrate5To6(dbConnection);
|
|
this._dbMigrate6To7(dbConnection);
|
|
this._dbMigrate7To8(dbConnection);
|
|
this._dbMigrate8To9(dbConnection);
|
|
this._dbMigrate9To10(dbConnection);
|
|
this._dbMigrate10To11(dbConnection);
|
|
this._dbMigrate11To12(dbConnection);
|
|
this._dbMigrate12To13(dbConnection);
|
|
this._dbMigrate13To14(dbConnection);
|
|
},
|
|
|
|
_dbMigrate6To14: function(dbConnection) {
|
|
this._dbMigrate6To7(dbConnection);
|
|
this._dbMigrate7To8(dbConnection);
|
|
this._dbMigrate8To9(dbConnection);
|
|
this._dbMigrate9To10(dbConnection);
|
|
this._dbMigrate10To11(dbConnection);
|
|
this._dbMigrate11To12(dbConnection);
|
|
this._dbMigrate12To13(dbConnection);
|
|
this._dbMigrate13To14(dbConnection);
|
|
},
|
|
|
|
_dbMigrate7To14: function(dbConnection) {
|
|
this._dbMigrate7To8(dbConnection);
|
|
this._dbMigrate8To9(dbConnection);
|
|
this._dbMigrate9To10(dbConnection);
|
|
this._dbMigrate10To11(dbConnection);
|
|
this._dbMigrate11To12(dbConnection);
|
|
this._dbMigrate12To13(dbConnection);
|
|
this._dbMigrate13To14(dbConnection);
|
|
},
|
|
|
|
_dbMigrate8To14: function(dbConnection) {
|
|
this._dbMigrate8To9(dbConnection);
|
|
this._dbMigrate9To10(dbConnection);
|
|
this._dbMigrate10To11(dbConnection);
|
|
this._dbMigrate11To12(dbConnection);
|
|
this._dbMigrate12To13(dbConnection);
|
|
this._dbMigrate13To14(dbConnection);
|
|
},
|
|
|
|
_dbMigrate9To14: function(dbConnection) {
|
|
this._dbMigrate9To10(dbConnection);
|
|
this._dbMigrate10To11(dbConnection);
|
|
this._dbMigrate11To12(dbConnection);
|
|
this._dbMigrate12To13(dbConnection);
|
|
this._dbMigrate13To14(dbConnection);
|
|
},
|
|
|
|
_dbMigrate10To14: function(dbConnection) {
|
|
this._dbMigrate10To11(dbConnection);
|
|
this._dbMigrate11To12(dbConnection);
|
|
this._dbMigrate12To13(dbConnection);
|
|
this._dbMigrate13To14(dbConnection);
|
|
},
|
|
|
|
_dbMigrate11To14: function(dbConnection) {
|
|
this._dbMigrate11To12(dbConnection);
|
|
this._dbMigrate12To13(dbConnection);
|
|
this._dbMigrate13To14(dbConnection);
|
|
},
|
|
|
|
_dbMigrate12To14: function(dbConnection) {
|
|
this._dbMigrate12To13(dbConnection);
|
|
this._dbMigrate13To14(dbConnection);
|
|
},
|
|
|
|
_dbMigrate4To5: function(aDBConnection) {
|
|
aDBConnection.executeSimpleSQL(
|
|
"UPDATE sources SET lastRefreshed = lastRefreshed / 1000 / 86400 + 2440587.5"
|
|
);
|
|
aDBConnection.executeSimpleSQL(
|
|
"UPDATE messages SET timestamp = timestamp / 1000 / 86400 + 2440587.5"
|
|
);
|
|
aDBConnection.executeSimpleSQL(
|
|
"ALTER TABLE messages ADD COLUMN received REAL"
|
|
);
|
|
},
|
|
|
|
_dbMigrate5To6: function(aDBConnection) {
|
|
// Rename the old parts table.
|
|
aDBConnection.executeSimpleSQL("ALTER TABLE parts RENAME TO partsOld");
|
|
|
|
// Create the new parts and partsText tables.
|
|
this._dbCreateTable(aDBConnection, "parts", this._dbSchema.tables.parts);
|
|
this._dbCreateTable(aDBConnection, "partsText", this._dbSchema.tables.partsText);
|
|
|
|
// Copy the data from the old to the new parts table. It may look like
|
|
// the tables are equivalent, but the old table was fulltext and didn't have
|
|
// an "id" column (which we don't reference here because it gets populated
|
|
// automagically as an AUTOINCREMENT PRIMARY KEY column).
|
|
aDBConnection.executeSimpleSQL(
|
|
"INSERT INTO parts(messageID, content, mediaType, partType, baseURI, languageTag) " +
|
|
"SELECT messageID, content, mediaType, partType, baseURI, languageCode " +
|
|
"FROM partsOld"
|
|
);
|
|
|
|
// Insert data into the new partsText table.
|
|
let selectStatement = this.createStatement("SELECT id, content, mediaType FROM parts", aDBConnection);
|
|
let insertStatement = this.createStatement("INSERT INTO partsText (docid, content) VALUES (:docid, :content)", aDBConnection);
|
|
try {
|
|
while (selectStatement.step()) {
|
|
let plainText = selectStatement.row.content;
|
|
|
|
switch (selectStatement.row.mediaType) {
|
|
case "text/html":
|
|
case "application/xhtml+xml":
|
|
// Use nsIFeedTextConstruct to convert the markup to plaintext.
|
|
let (construct = Cc["@mozilla.org/feed-textconstruct;1"].
|
|
createInstance(Ci.nsIFeedTextConstruct)) {
|
|
construct.text = selectStatement.row.content;
|
|
construct.type = TEXT_CONSTRUCT_TYPES[selectStatement.row.mediaType];
|
|
plainText = construct.plainText();
|
|
}
|
|
// Now that we've converted the markup to plain text, fall through
|
|
// to the text/plain case that inserts the data into the database.
|
|
|
|
case "text/plain":
|
|
// Give the fulltext record the same doc ID as the row ID of the parts
|
|
// record so we can join them together to get the part (and thence the
|
|
// message) when doing a fulltext search.
|
|
insertStatement.params.docid = selectStatement.row.id;
|
|
insertStatement.params.content = plainText;
|
|
insertStatement.execute();
|
|
break;
|
|
|
|
default:
|
|
// It isn't a type we understand, so don't do anything with it.
|
|
// XXX If it's text/*, shouldn't we fulltext index it anyway?
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
selectStatement.reset();
|
|
}
|
|
|
|
// Drop the old parts table.
|
|
aDBConnection.executeSimpleSQL("DROP TABLE partsOld");
|
|
},
|
|
|
|
/**
|
|
* Migrate the database schema from version 6 to version 7.
|
|
*
|
|
* This doesn't actually change the physical database schema, it just removes
|
|
* subjects from Twitter messages, since it no longer makes sense to store
|
|
* tweets as both the subjects and the content of messages now that the views
|
|
* support messages that don't necessarily have subjects.
|
|
*/
|
|
_dbMigrate6To7: function(aDBConnection) {
|
|
aDBConnection.executeSimpleSQL(
|
|
"UPDATE messages SET subject = NULL WHERE sourceID IN " +
|
|
"(SELECT id FROM sources WHERE type = 'SnowlTwitter')"
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Migrate the database schema from version 7 to version 8.
|
|
*/
|
|
_dbMigrate7To8: function(aDBConnection) {
|
|
aDBConnection.executeSimpleSQL("ALTER TABLE sources ADD COLUMN username TEXT");
|
|
},
|
|
|
|
/**
|
|
* Migrate the database schema from version 8 to version 9.
|
|
*/
|
|
_dbMigrate8To9: function(dbConnection) {
|
|
// Move the old messages table out of the way.
|
|
dbConnection.executeSimpleSQL("ALTER TABLE messages RENAME TO messagesOld");
|
|
|
|
// Create the new messages table and its index.
|
|
this._dbCreateTable(dbConnection, "messages", this._dbSchema.tables.messages);
|
|
this._dbCreateIndex(dbConnection, this._dbSchema.indexes[0]);
|
|
|
|
// Copy messages that aren't from Twitter.
|
|
dbConnection.executeSimpleSQL(
|
|
"INSERT INTO messages(id, sourceID, externalID, subject, authorID, " +
|
|
" timestamp, received, link, current, read) " +
|
|
"SELECT messagesOld.id, sourceID, externalID, subject, " +
|
|
" authorID, timestamp, received, link, current, read " +
|
|
"FROM messagesOld JOIN sources ON messagesOld.sourceID = sources.id " +
|
|
"WHERE sources.type != 'SnowlTwitter'"
|
|
);
|
|
|
|
// Copy messages that are from Twitter, converting their externalIDs
|
|
// to integers in the process.
|
|
dbConnection.executeSimpleSQL(
|
|
"INSERT INTO messages(id, sourceID, externalID, subject, authorID, " +
|
|
" timestamp, received, link, current, read) " +
|
|
"SELECT messagesOld.id, sourceID, CAST(externalID AS INTEGER), subject, " +
|
|
" authorID, timestamp, received, link, current, read " +
|
|
"FROM messagesOld JOIN sources ON messagesOld.sourceID = sources.id " +
|
|
"WHERE sources.type = 'SnowlTwitter'"
|
|
);
|
|
|
|
// Drop the old messages table.
|
|
dbConnection.executeSimpleSQL("DROP TABLE messagesOld");
|
|
},
|
|
|
|
/**
|
|
* Migrate the database schema from version 9 to version 10.
|
|
*/
|
|
_dbMigrate9To10: function(dbConnection) {
|
|
// Create the index on the messageID column in the parts table.
|
|
this._dbCreateIndex(dbConnection, this._dbSchema.indexes[1]);
|
|
},
|
|
|
|
/**
|
|
* Migrate the database schema from version 10 to version 11.
|
|
*/
|
|
_dbMigrate10To11: function(dbConnection) {
|
|
// Update the icon URLs for the sources and authors collections.
|
|
// XXX There should be a better way to store and update these URLs.
|
|
// For example, we could generate them dynamically in code based on
|
|
// the type of collection.
|
|
dbConnection.executeSimpleSQL("UPDATE collections SET iconURL = 'chrome://snowl/skin/livemarkFolder-16.png' WHERE groupIDColumn = 'sources.id'");
|
|
dbConnection.executeSimpleSQL("UPDATE collections SET iconURL = 'chrome://snowl/skin/person-16.png' WHERE groupIDColumn = 'authors.id'");
|
|
},
|
|
|
|
/**
|
|
* Migrate the database schema from version 11 to version 12.
|
|
*/
|
|
_dbMigrate11To12: function(dbConnection) {
|
|
dbConnection.executeSimpleSQL("ALTER TABLE sources ADD COLUMN placeID INTEGER");
|
|
dbConnection.executeSimpleSQL("ALTER TABLE people ADD COLUMN placeID INTEGER");
|
|
},
|
|
|
|
/**
|
|
* Migrate the database schema from version 12 to 13.
|
|
*/
|
|
_dbMigrate12To13: function(dbConnection) {
|
|
dbConnection.executeSimpleSQL("DROP TABLE metadata");
|
|
dbConnection.executeSimpleSQL("DROP TABLE personMetadata");
|
|
dbConnection.executeSimpleSQL("DROP TABLE attributes");
|
|
},
|
|
|
|
/**
|
|
* Migrate the database schema from version 13 to 14.
|
|
*/
|
|
_dbMigrate13To14: function(dbConnection) {
|
|
dbConnection.executeSimpleSQL("ALTER TABLE sources ADD COLUMN attributes TEXT DEFAULT '{}'");
|
|
|
|
// Move the old messages table out of the way.
|
|
this._dbDropIndex(dbConnection, this._dbSchema.indexes[0]);
|
|
dbConnection.executeSimpleSQL("ALTER TABLE messages RENAME TO messagesOld");
|
|
|
|
// Create the new messages table and its index.
|
|
this._dbCreateTable(dbConnection, "messages", this._dbSchema.tables.messages);
|
|
this._dbCreateIndex(dbConnection, this._dbSchema.indexes[0]);
|
|
|
|
// Copy messages, converting their read and current flags to integers.
|
|
dbConnection.executeSimpleSQL(
|
|
"INSERT INTO messages(id, sourceID, externalID, subject, authorID, " +
|
|
" timestamp, received, link, current, read) " +
|
|
"SELECT messagesOld.id, sourceID, externalID, subject, " +
|
|
" authorID, timestamp, received, link, " +
|
|
" CAST(current AS INTEGER), CAST(read AS INTEGER) " +
|
|
"FROM messagesOld JOIN sources ON messagesOld.sourceID = sources.id "
|
|
);
|
|
|
|
// Drop the old messages table.
|
|
dbConnection.executeSimpleSQL("DROP TABLE messagesOld");
|
|
},
|
|
|
|
get _selectHasSourceStatement() {
|
|
let statement = this.createStatement(
|
|
"SELECT name FROM sources WHERE machineURI = :machineURI"
|
|
);
|
|
this.__defineGetter__("_selectHasSourceStatement", function() { return statement });
|
|
return this._selectHasSourceStatement;
|
|
},
|
|
|
|
selectHasSource: function(aMachineURI) {
|
|
let name;
|
|
|
|
try {
|
|
this._selectHasSourceStatement.params.machineURI = aMachineURI;
|
|
if (this._selectHasSourceStatement.step())
|
|
name = this._selectHasSourceStatement.row["name"];
|
|
}
|
|
finally {
|
|
this._selectHasSourceStatement.reset();
|
|
}
|
|
|
|
return name;
|
|
},
|
|
|
|
get _selectHasSourceUsernameStatement() {
|
|
let statement = this.createStatement(
|
|
"SELECT name, username FROM sources " +
|
|
"WHERE machineURI = :machineURI AND username = :username"
|
|
);
|
|
this.__defineGetter__("_selectHasSourceUsernameStatement", function() { return statement });
|
|
return this._selectHasSourceUsernameStatement;
|
|
},
|
|
|
|
selectHasSourceUsername: function(aMachineURI, aUsername) {
|
|
let name, username;
|
|
|
|
try {
|
|
this._selectHasSourceUsernameStatement.params.machineURI = aMachineURI;
|
|
this._selectHasSourceUsernameStatement.params.username = aUsername;
|
|
if (this._selectHasSourceUsernameStatement.step()) {
|
|
name = this._selectHasSourceUsernameStatement.row["name"];
|
|
username = this._selectHasSourceUsernameStatement.row["username"];
|
|
}
|
|
}
|
|
finally {
|
|
this._selectHasSourceUsernameStatement.reset();
|
|
}
|
|
|
|
return [name, username];
|
|
},
|
|
|
|
get _selectHasIdentityMessageStatement() {
|
|
let statement = this.createStatement(
|
|
"SELECT 1 FROM messages " +
|
|
"WHERE authorID = :authorID AND " +
|
|
" current <> " + MESSAGE_CURRENT_PENDING_PURGE
|
|
);
|
|
this.__defineGetter__("_selectHasIdentityMessageStatement", function() { return statement });
|
|
return this._selectHasIdentityMessageStatement;
|
|
},
|
|
|
|
selectHasIdentityMessage: function(aAuthorID) {
|
|
let hasMessage = false;
|
|
try {
|
|
this._selectHasIdentityMessageStatement.params.authorID = aAuthorID;
|
|
if (this._selectHasIdentityMessageStatement.step())
|
|
hasMessage = true;
|
|
}
|
|
finally {
|
|
this._selectHasIdentityMessageStatement.reset();
|
|
}
|
|
|
|
return hasMessage;
|
|
},
|
|
|
|
get _selectHasAuthorIdentityStatement() {
|
|
let statement = this.createStatement(
|
|
"SELECT 1 FROM identities " +
|
|
"WHERE personID = :authorID"
|
|
);
|
|
this.__defineGetter__("_selectHasAuthorIdentityStatement", function() { return statement });
|
|
return this._selectHasAuthorIdentityStatement;
|
|
},
|
|
|
|
selectHasAuthorIdentity: function(aAuthorID) {
|
|
let hasIdentity = false;
|
|
try {
|
|
this._selectHasAuthorIdentityStatement.params.authorID = aAuthorID;
|
|
if (this._selectHasAuthorIdentityStatement.step())
|
|
hasIdentity = true;
|
|
}
|
|
finally {
|
|
this._selectHasAuthorIdentityStatement.reset();
|
|
}
|
|
|
|
return hasIdentity;
|
|
},
|
|
|
|
get _insertSourceTypeStatement() {
|
|
let statement = this.createStatement(
|
|
"INSERT INTO sources ( name, type, machineURI, lastRefreshed, attributes) " +
|
|
"VALUES ( :name, :type, :machineURI, :lastRefreshed, :attributes)"
|
|
);
|
|
this.__defineGetter__("_insertSourceTypeStatement", function() { return statement });
|
|
return this._insertSourceTypeStatement;
|
|
},
|
|
|
|
/**
|
|
* Insert a record into the sources table, used for inserting type records only.
|
|
*
|
|
* @param aName {string} the name
|
|
* @param aType {string} indicating a source type - 'SnowlAccountType'
|
|
* @param aMachineURI {string} name of the constructor, the type of source
|
|
* @param aAttributes {string} default attributes for this source type
|
|
*
|
|
* @returns {integer} the ID of the newly-created record
|
|
*/
|
|
insertSourceType: function(aName, aType, aMachineURI, aLastRefreshed, aAttributes) {
|
|
try {
|
|
this._insertSourceTypeStatement.params.name = aName;
|
|
this._insertSourceTypeStatement.params.type = aType;
|
|
this._insertSourceTypeStatement.params.machineURI = aMachineURI;
|
|
this._insertSourceTypeStatement.params.lastRefreshed = aLastRefreshed;
|
|
this._insertSourceTypeStatement.params.attributes = JSON.stringify(aAttributes);
|
|
this._insertSourceTypeStatement.execute();
|
|
}
|
|
finally {
|
|
this._insertSourceTypeStatement.reset();
|
|
}
|
|
|
|
return this.dbConnection.lastInsertRowID;
|
|
},
|
|
|
|
get _insertMessageStatement() {
|
|
let statement = this.createStatement(
|
|
"INSERT INTO messages(sourceID, externalID, subject, authorID, timestamp, received, link) \
|
|
VALUES (:sourceID, :externalID, :subject, :authorID, :timestamp, :received, :link)"
|
|
);
|
|
this.__defineGetter__("_insertMessageStatement", function() { return statement });
|
|
return this._insertMessageStatement;
|
|
},
|
|
|
|
/**
|
|
* Insert a record into the messages table.
|
|
*
|
|
* @param aSourceID {integer} the record ID of the message source
|
|
* @param aExternalID {string} the external ID of the message
|
|
* @param aSubject {string} the title of the message
|
|
* @param aAuthorID {string} the author of the message
|
|
* @param aTimestamp {real} the Julian date when the message was sent
|
|
* @param aReceived {real} the Julian date when the message was received
|
|
* @param aLink {string} a link to the content of the message,
|
|
* if the content is hosted on a server
|
|
*
|
|
* @returns {integer} the ID of the newly-created record
|
|
*/
|
|
insertMessage: function(aSourceID, aExternalID, aSubject, aAuthorID, aTimestamp, aReceived, aLink) {
|
|
try {
|
|
this._insertMessageStatement.params.sourceID = aSourceID;
|
|
this._insertMessageStatement.params.externalID = aExternalID;
|
|
this._insertMessageStatement.params.subject = aSubject;
|
|
this._insertMessageStatement.params.authorID = aAuthorID;
|
|
this._insertMessageStatement.params.timestamp = aTimestamp;
|
|
this._insertMessageStatement.params.received = aReceived;
|
|
this._insertMessageStatement.params.link = aLink;
|
|
this._insertMessageStatement.execute();
|
|
}
|
|
finally {
|
|
this._insertMessageStatement.reset();
|
|
}
|
|
|
|
return this.dbConnection.lastInsertRowID;
|
|
},
|
|
|
|
get _selectIdentitiesSourceIDStatement() {
|
|
let statement = this.createStatement(
|
|
"SELECT sourceID, externalID FROM identities WHERE personID = :id"
|
|
);
|
|
this.__defineGetter__("_selectIdentitiesSourceIDStatement",
|
|
function() { return statement });
|
|
return this._selectIdentitiesSourceIDStatement;
|
|
},
|
|
|
|
/**
|
|
* Get sourceID for a people table entry from identities table.
|
|
*
|
|
* @param aID {integer} the record ID of the people entry, which should be
|
|
* tested against the peopleID value in identities
|
|
*
|
|
* @returns {integer} the sourceID of the people record
|
|
*/
|
|
selectIdentitiesSourceID: function(aID) {
|
|
let sourceID, externalID;
|
|
|
|
try {
|
|
this._selectIdentitiesSourceIDStatement.params.id = aID;
|
|
if (this._selectIdentitiesSourceIDStatement.step()) {
|
|
sourceID = this._selectIdentitiesSourceIDStatement.row["sourceID"];
|
|
externalID = this._selectIdentitiesSourceIDStatement.row["externalID"];
|
|
}
|
|
}
|
|
finally {
|
|
this._selectIdentitiesSourceIDStatement.reset();
|
|
}
|
|
|
|
return [sourceID, externalID];
|
|
},
|
|
|
|
_collectionStatsStatement: function(aType) {
|
|
let query;
|
|
switch (aType) {
|
|
case "all":
|
|
query = "SELECT id AS collectionID, " +
|
|
" COUNT(messages.id) AS total, " +
|
|
" SUM(read) AS read, " +
|
|
" SUM(ROUND(read/2,0)) AS new " +
|
|
"FROM messages " +
|
|
"WHERE (current = " + MESSAGE_NON_CURRENT + " OR " +
|
|
" current = " + MESSAGE_CURRENT + ") ";
|
|
break;
|
|
case "source":
|
|
query = "SELECT sourceID AS collectionID, " +
|
|
" COUNT(messages.id) AS total, " +
|
|
" SUM(read) AS read, " +
|
|
" SUM(ROUND(read/2,0)) AS new " +
|
|
"FROM messages " +
|
|
"WHERE (current = " + MESSAGE_NON_CURRENT + " OR " +
|
|
" current = " + MESSAGE_CURRENT + ") GROUP BY collectionID";
|
|
break;
|
|
case "author":
|
|
query = "SELECT authorID, identities.id, identities.personID AS collectionID, " +
|
|
" COUNT(messages.id) AS total, " +
|
|
" SUM(read) AS read, " +
|
|
" SUM(ROUND(read/2,0)) AS new " +
|
|
"FROM messages " +
|
|
"LEFT JOIN identities ON messages.authorID = identities.id " +
|
|
"WHERE (current = " + MESSAGE_NON_CURRENT + " OR " +
|
|
" current = " + MESSAGE_CURRENT + ") GROUP BY collectionID";
|
|
break;
|
|
}
|
|
|
|
return this.createStatement(query);
|
|
},
|
|
|
|
collectionStatsByCollectionID: function() {
|
|
// Stats object for collections tree properties.
|
|
let statement, type, types = ["all", "source"];
|
|
let collectionID, Total, Read, New, collectionStats = {};
|
|
|
|
if (SnowlPlaces.collectionsAuthorsID != -1)
|
|
// Authors collection being built, also calc author stats.
|
|
types.push("author");
|
|
|
|
try {
|
|
for each (type in types) {
|
|
statement = this._collectionStatsStatement(type);
|
|
while (statement.step()) {
|
|
if (statement.row["collectionID"] == null)
|
|
continue;
|
|
|
|
collectionID = type == "all" ?
|
|
"all" : type[0] + statement.row["collectionID"];
|
|
|
|
Total = statement.row["total"] ? statement.row["total"] : 0;
|
|
Read = statement.row["read"] ? statement.row["read"] : 0;
|
|
New = statement.row["new"] ? statement.row["new"] : 0;
|
|
collectionStats[collectionID] = {
|
|
t: Total,
|
|
u: Total - Read + MESSAGE_NEW * New,
|
|
n: New
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch(ex) {
|
|
this._log.error(ex);
|
|
}
|
|
finally {
|
|
statement.reset();
|
|
}
|
|
|
|
return collectionStats;
|
|
}
|
|
|
|
};
|
|
|
|
/**
|
|
* Parsed query uri associated with a Places collection row.
|
|
*
|
|
* @param aUri (string) - query string contained in the places item's uri.
|
|
*/
|
|
function SnowlQuery(aUri) {
|
|
this.queryUri = decodeURI(aUri);
|
|
if (this.queryUri) {
|
|
if (this.queryUri.indexOf("place:") != -1) {
|
|
this.queryProtocol = "place:";
|
|
this.queryFolder = this.queryUri.indexOf("folder=") != -1 ?
|
|
this.queryUri.split("folder=")[1].split("&")[0] : null;
|
|
}
|
|
else if (this.queryUri.indexOf("snowl:") != -1) {
|
|
this.queryProtocol = "snowl:";
|
|
this.querySourceID = this.queryUri.split("snowl:sId=")[1].split("&")[0];
|
|
this.queryID = this.queryUri.split(".id=")[1].split("&")[0];
|
|
if (this.queryUri.indexOf("&a.id=") != -1) {
|
|
this.queryGroupIDColumn = "people.id";
|
|
this.queryTypeAuthor = true;
|
|
}
|
|
else if (this.queryUri.indexOf("&s.id=") != -1) {
|
|
this.queryGroupIDColumn = "sources.id";
|
|
this.queryTypeSource = true;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
SnowlQuery.prototype = {
|
|
queryUri: null,
|
|
queryProtocol: null,
|
|
queryID: null,
|
|
querySourceID: null,
|
|
queryFolder: null,
|
|
queryTypeSource: false,
|
|
queryTypeAuthor: false,
|
|
queryGroupIDColumn: null,
|
|
};
|
|
|
|
/**
|
|
* Places functions for Snowl
|
|
*/
|
|
let SnowlPlaces = {
|
|
get _log() {
|
|
let logger = Log4Moz.repository.getLogger("Snowl.SnowlPlaces");
|
|
this.__defineGetter__("_log", function() logger);
|
|
return this._log;
|
|
},
|
|
|
|
_placesVersion: 3,
|
|
_placesConverted: false,
|
|
_placesInitialized: false,
|
|
|
|
getPlacesVersion: function(snowlPlacesRoot) {
|
|
let verInfo = PlacesUtils.annotations
|
|
.getItemAnnotation(snowlPlacesRoot,
|
|
this.SNOWL_ROOT_ANNO);
|
|
this._log.info("getPlacesVersion: " + verInfo);
|
|
let curVer = verInfo.split("version=")[1] ?
|
|
verInfo.split("version=")[1].split("&")[0] : null;
|
|
let curConv = verInfo.split("converted=")[1] ?
|
|
verInfo.split("converted=")[1] : false;
|
|
this._placesConverted = curConv == "true" ? true : false;
|
|
return curVer;
|
|
},
|
|
|
|
setPlacesVersion: function(snowlPlacesRoot) {
|
|
let verInfo = "version=" + this._placesVersion +
|
|
"&converted=" + this._placesConverted;
|
|
this._log.info("setPlacesVersion: " + verInfo);
|
|
PlacesUtils.annotations.
|
|
setItemAnnotation(snowlPlacesRoot,
|
|
this.SNOWL_ROOT_ANNO,
|
|
verInfo,
|
|
0,
|
|
this.EXPIRE_NEVER);
|
|
},
|
|
|
|
SNOWL_ROOT_ANNO: "Snowl",
|
|
SNOWL_COLLECTIONS_ANNO: "Snowl/Collections",
|
|
SNOWL_COLLECTIONS_SYSTEM_ANNO: "Snowl/Collections/System",
|
|
SNOWL_COLLECTIONS_SOURCE_ANNO: "Snowl/Collections/Source",
|
|
SNOWL_COLLECTIONS_AUTHOR_ANNO: "Snowl/Collections/Author",
|
|
SNOWL_USER_ANNO: "Snowl/User",
|
|
SNOWL_USER_VIEW_ANNO: "Snowl/User/View",
|
|
SNOWL_USER_VIEWLIST_ANNO: "Snowl/User/ViewList",
|
|
SNOWL_PROPERTIES_ANNO: "Snowl/Properties",
|
|
|
|
EXCLUDE_FROM_BACKUP_ANNO: "places/excludeFromBackup",
|
|
EXPIRE_NEVER: PlacesUtils.annotations.EXPIRE_NEVER,
|
|
DEFAULT_INDEX: PlacesUtils.bookmarks.DEFAULT_INDEX,
|
|
|
|
queryDefault: "place:queryType=1&expandQueries=0&excludeReadOnlyFolders=0&folder=",
|
|
querySources: "place:queryType=1&expandQueries=0&sort=1&folder=",
|
|
queryAuthors: "place:queryType=1&expandQueries=0&sort=1&folder=",
|
|
queryCustom: "place:queryType=1&expandQueries=0&folder=",
|
|
|
|
get collectionsSystemID() {
|
|
delete this.collectionsSystemID;
|
|
return this.collectionsSystemID = this.snowlPlacesQueries["snowlCollectionsSystem"];
|
|
},
|
|
set collectionsSystemID(val) {
|
|
delete this.collectionsSystemID;
|
|
return this.collectionsSystemID = val;
|
|
},
|
|
|
|
get collectionsSourcesID() {
|
|
delete this.collectionsSourcesID;
|
|
return this.collectionsSourcesID = this.snowlPlacesQueries["snowlCollectionsSources"];
|
|
},
|
|
set collectionsSourcesID(val) {
|
|
delete this.collectionsSourcesID;
|
|
return this.collectionsSourcesID = val;
|
|
},
|
|
|
|
get collectionsAuthorsID() {
|
|
// Lack of optional Authors collection indicated by -1 value.
|
|
delete this.collectionsAuthorsID;
|
|
return this.collectionsAuthorsID = this.snowlPlacesQueries["snowlCollectionsAuthors"] ?
|
|
this.snowlPlacesQueries["snowlCollectionsAuthors"] : -1;
|
|
},
|
|
set collectionsAuthorsID(val) {
|
|
delete this.collectionsAuthorsID;
|
|
return this.collectionsAuthorsID = val;
|
|
},
|
|
|
|
get collectionsAllID() {
|
|
delete this.collectionsAllID;
|
|
return this.collectionsAllID = this.snowlPlacesQueries["snowlCollectionsAll"];
|
|
},
|
|
set collectionsAllID(val) {
|
|
delete this.collectionsAllID;
|
|
return this.collectionsAllID = val;
|
|
},
|
|
|
|
get userRootID() {
|
|
delete this.userRootID;
|
|
return this.userRootID = this.snowlPlacesQueries["snowlUserRoot"];
|
|
},
|
|
set userRootID(val) {
|
|
delete this.userRootID;
|
|
return this.userRootID = val;
|
|
},
|
|
|
|
/**
|
|
* Add a Places bookmark for a snowl source or author collection
|
|
*
|
|
* @aTable - messages.sqlite sources or people table
|
|
* @aId - table id of source or author record
|
|
* @aName - name
|
|
* @aMachineURI - url
|
|
* @aUsername - externalID from people table
|
|
* @aIconURI - favicon
|
|
* @aSourceId - sourceId of source or author record
|
|
*/
|
|
persistPlace: function(aTable, aId, aName, aMachineURI, aUsername, aIconURI, aSourceId) {
|
|
let uri, parent, anno, properties, placeID;
|
|
if (aTable == "sources") {
|
|
uri = URI("snowl:sId=" + aSourceId +
|
|
"&s.id=" + aId +
|
|
"&u=" + aMachineURI.spec +
|
|
"&");
|
|
parent = this.collectionsSourcesID;
|
|
anno = this.SNOWL_COLLECTIONS_SOURCE_ANNO;
|
|
properties = "source";
|
|
}
|
|
else if (aTable == "people") {
|
|
uri = URI("snowl:sId=" + aSourceId +
|
|
"&a.id=" + aId +
|
|
"&e=" + aUsername +
|
|
"&");
|
|
parent = this.collectionsAuthorsID;
|
|
anno = this.SNOWL_COLLECTIONS_AUTHOR_ANNO;
|
|
properties = "author";
|
|
|
|
// If Authors collection not built, return.
|
|
if (this.collectionsAuthorsID == -1)
|
|
return null;
|
|
}
|
|
else
|
|
return null;
|
|
|
|
try {
|
|
placeID = PlacesUtils.bookmarks.
|
|
insertBookmark(parent,
|
|
uri,
|
|
this.DEFAULT_INDEX,
|
|
aName);
|
|
PlacesUtils.annotations.
|
|
// setPageAnnotation(uri,
|
|
setItemAnnotation(placeID,
|
|
anno,
|
|
properties,
|
|
0,
|
|
this.EXPIRE_NEVER);
|
|
|
|
if (aIconURI)
|
|
// Skip if no icon URI, default icon set via css.
|
|
PlacesUtils.favicons.
|
|
setAndLoadFaviconForPage(uri,
|
|
aIconURI,
|
|
true);
|
|
}
|
|
catch(ex) {
|
|
this._log.error("persistPlace: parentId:aName:uri - " +
|
|
parent + " : " + aName + " : " + uri.spec );
|
|
this._log.error(ex);
|
|
}
|
|
|
|
return placeID;
|
|
},
|
|
|
|
/**
|
|
* Remove bookmarks based on full or partial uri
|
|
*
|
|
* @aUri - full or partial uri to remove by
|
|
* @aPrefix - if true, find by prefixed partial uri
|
|
*/
|
|
removePlacesItemsByURI: function (aUri, aPrefix) {
|
|
this._log.debug("removePlacesItemsByURI: aUri:aPrefix - " + aUri + " : " + aPrefix);
|
|
let node, bookmarkIds = [], uniqueIds = [];
|
|
let query = PlacesUtils.history.getNewQuery();
|
|
query.setFolders([SnowlPlaces.collectionsSystemID], 1);
|
|
query.uri = URI(aUri);
|
|
query.uriIsPrefix = aPrefix ? aPrefix : false;
|
|
let options = PlacesUtils.history.getNewQueryOptions();
|
|
options.queryType = options.QUERY_TYPE_BOOKMARKS;
|
|
|
|
let rootNode = PlacesUtils.history.executeQuery(query, options).root;
|
|
rootNode.containerOpen = true;
|
|
|
|
// Multiple identical uris return multiple itemIds in one call, so
|
|
// bookmarkIds may have duplicates. Also, close node before any deletes.
|
|
for (let i = 0; i < rootNode.childCount; i ++) {
|
|
node = rootNode.getChild(i);
|
|
bookmarkIds = bookmarkIds.concat(PlacesUtils.bookmarks.
|
|
getBookmarkIdsForURI(URI(node.uri), {}));
|
|
}
|
|
rootNode.containerOpen = false;
|
|
|
|
// Remove duplicates from the array, if any
|
|
bookmarkIds.forEach(function(itemid) {
|
|
if (uniqueIds.indexOf(itemid, 0) < 0)
|
|
uniqueIds.push(itemid);
|
|
})
|
|
|
|
// Remove the bookmarks
|
|
uniqueIds.forEach(function(itemid) {
|
|
PlacesUtils.bookmarks.removeItem(itemid);
|
|
})
|
|
},
|
|
|
|
/**
|
|
* Called from Properties dialog on save, for View adds and View and Source/Author
|
|
* name changes, and from setCellText tree inline rename.
|
|
*
|
|
* @aNode - node
|
|
* @aUri - uri
|
|
* @aNewTitle - new title
|
|
*/
|
|
renamePlace: function(aItemId, aUri, aNewTitle) {
|
|
let itemChangedObj = {
|
|
itemId: aItemId,
|
|
type: null,
|
|
property: "title",
|
|
uri: aUri,
|
|
title: aNewTitle
|
|
}
|
|
|
|
if (PlacesUtils.annotations.
|
|
itemHasAnnotation(aItemId,
|
|
this.SNOWL_USER_VIEWLIST_ANNO)) {
|
|
// View shortcut folder.
|
|
itemChangedObj.type = "view";
|
|
}
|
|
else {
|
|
let parentId = PlacesUtils.bookmarks.getFolderIdForItem(aItemId);
|
|
if (parentId == this.collectionsSourcesID ||
|
|
parentId == this.collectionsAuthorsID)
|
|
// Source/author folder.
|
|
itemChangedObj.type = "collection";
|
|
else
|
|
return;
|
|
}
|
|
|
|
Observers.notify("itemchanged", itemChangedObj);
|
|
return;
|
|
},
|
|
|
|
/**
|
|
* Build a map of queryIds (any unique name) and itemIds for an annotation.
|
|
*
|
|
* @aAnno - annotation to map
|
|
*/
|
|
buildNameItemMap: function(aAnno) {
|
|
let map = {};
|
|
let items = PlacesUtils.annotations
|
|
.getItemsWithAnnotation(aAnno, {});
|
|
for (var i=0; i < items.length; i++) {
|
|
let queryName = PlacesUtils.annotations.
|
|
getItemAnnotation(items[i], aAnno);
|
|
map[queryName] = items[i];
|
|
this._log.debug("buildNameItemMap: " + queryName + " - " + items[i]);
|
|
}
|
|
|
|
return map;
|
|
},
|
|
|
|
resetNameItemMap: function() {
|
|
this.collectionsSystemID = this.snowlPlacesQueries["snowlCollectionsSystem"];
|
|
this.collectionsSourcesID = this.snowlPlacesQueries["snowlCollectionsSources"];
|
|
this.collectionsAuthorsID = this.snowlPlacesQueries["snowlCollectionsAuthors"];
|
|
this.collectionsAllID = this.snowlPlacesQueries["snowlCollectionsAll"];
|
|
this.userRootID = this.snowlPlacesQueries["snowlUserRoot"];
|
|
},
|
|
|
|
// Init snowl Places structure, delay to allow logger to set up.
|
|
init: function() {
|
|
// Only do once for session.
|
|
if (this._placesInitialized)
|
|
return;
|
|
|
|
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
let callback = { notify: function(aTimer) { SnowlPlaces.delayedInit() } };
|
|
timer.initWithCallback(callback, 10, Ci.nsITimer.TYPE_ONE_SHOT);
|
|
},
|
|
|
|
// Check for snowl Places structure and create if not found.
|
|
delayedInit: function() {
|
|
let items, itemID, collsysID, colluserID;
|
|
let snowlPlacesRoot = -1;
|
|
items = PlacesUtils.annotations
|
|
.getItemsWithAnnotation(this.SNOWL_USER_ANNO, {});
|
|
|
|
// Check for collections user root.
|
|
if (items.length != 1 || items[0] == -1) {
|
|
// Not found - create user root, which will contain user defined Views.
|
|
// This folder must therefore be preserved across rebuilds and is backed
|
|
// up via Places in .json files. It is thus a child of the Places root.
|
|
// It is outside the rebuild process and any changes to it or its children
|
|
// must be considered separately in the context of user data.
|
|
this._log.info("init: Initializing Snowl Places User Root...");
|
|
colluserID = PlacesUtils.bookmarks.
|
|
createFolder(PlacesUtils.placesRootId,
|
|
"snowlUserRoot",
|
|
this.DEFAULT_INDEX);
|
|
PlacesUtils.annotations.
|
|
setItemAnnotation(colluserID,
|
|
this.SNOWL_USER_ANNO,
|
|
"snowlUserRoot",
|
|
0,
|
|
this.EXPIRE_NEVER);
|
|
PlacesUtils.annotations.
|
|
setItemAnnotation(colluserID,
|
|
this.SNOWL_COLLECTIONS_ANNO,
|
|
"snowlUserRoot",
|
|
0,
|
|
this.EXPIRE_NEVER);
|
|
|
|
// Create collections custom View folder.
|
|
itemID = PlacesUtils.bookmarks.
|
|
createFolder(colluserID,
|
|
"snowlUserView:" +
|
|
strings.get("customCollectionName"),
|
|
this.DEFAULT_INDEX);
|
|
PlacesUtils.annotations.
|
|
setItemAnnotation(itemID,
|
|
this.SNOWL_USER_VIEW_ANNO,
|
|
"snowlUserView",
|
|
0,
|
|
this.EXPIRE_NEVER);
|
|
}
|
|
|
|
// Check rest of snowl Places structure.
|
|
items = PlacesUtils.annotations
|
|
.getItemsWithAnnotation(this.SNOWL_ROOT_ANNO, {});
|
|
|
|
if (items.length > 1) {
|
|
// Something went wrong, we cannot have more than one left pane folder,
|
|
// remove all left pane folders and generate a correct new one.
|
|
items.forEach(function(aItem) {
|
|
PlacesUtils.bookmarks.removeItem(aItem);
|
|
});
|
|
}
|
|
else if (items.length == 1 && items[0] != -1) {
|
|
snowlPlacesRoot = items[0];
|
|
// Check snowl Places version
|
|
let version = this.getPlacesVersion(snowlPlacesRoot);
|
|
if (version != this._placesVersion ||
|
|
!this._placesConverted ||
|
|
SnowlDatastore._dbFileIsNew) {
|
|
// If version is not current or converted flag not set or the messages
|
|
// db has been newly created (on new install or file not found/corrupt),
|
|
// then rebuild the snowl Places structure.
|
|
PlacesUtils.bookmarks.removeItem(snowlPlacesRoot);
|
|
snowlPlacesRoot = -1;
|
|
this._placesConverted = false;
|
|
}
|
|
}
|
|
|
|
if (snowlPlacesRoot != -1) {
|
|
// Build the map.
|
|
delete this.snowlPlacesQueries;
|
|
this.snowlPlacesQueries = this.buildNameItemMap(this.SNOWL_COLLECTIONS_ANNO);
|
|
|
|
this._placesInitialized = true;
|
|
|
|
// Set the root itemId.
|
|
delete this.snowlPlacesFolderId;
|
|
return this.snowlPlacesFolderId = snowlPlacesRoot;
|
|
}
|
|
|
|
this._log.info("init: Rebuilding Snowl Places...");
|
|
|
|
// var callback = {
|
|
// runBatched: function(aUserData) {
|
|
// delete self.snowlPlacesQueries;
|
|
// self.snowlPlacesQueries = { };
|
|
|
|
// Create snowl Places root folder. This folder and thus its children
|
|
// are excluded from Places backup, as underlying data exists in the
|
|
// messages db and the structure would need to be rebuilt for any changes
|
|
// or for recovery etc.
|
|
snowlPlacesRoot = PlacesUtils.bookmarks.
|
|
createFolder(PlacesUtils.placesRootId,
|
|
"snowlRoot",
|
|
this.DEFAULT_INDEX);
|
|
PlacesUtils.annotations.
|
|
setItemAnnotation(snowlPlacesRoot,
|
|
this.SNOWL_ROOT_ANNO,
|
|
this._placesVersion,
|
|
0,
|
|
this.EXPIRE_NEVER);
|
|
PlacesUtils.annotations.
|
|
setItemAnnotation(snowlPlacesRoot,
|
|
this.EXCLUDE_FROM_BACKUP_ANNO,
|
|
1,
|
|
0,
|
|
this.EXPIRE_NEVER);
|
|
// Ensure immediate children can't be removed
|
|
PlacesUtils.bookmarks.setFolderReadonly(snowlPlacesRoot, true);
|
|
|
|
// Create collections system.
|
|
collsysID = PlacesUtils.bookmarks.
|
|
createFolder(snowlPlacesRoot,
|
|
"snowlCollectionsSystem",
|
|
this.DEFAULT_INDEX);
|
|
PlacesUtils.annotations.
|
|
setItemAnnotation(collsysID,
|
|
this.SNOWL_COLLECTIONS_ANNO,
|
|
"snowlCollectionsSystem",
|
|
0,
|
|
this.EXPIRE_NEVER);
|
|
PlacesUtils.annotations.
|
|
setItemAnnotation(collsysID,
|
|
this.SNOWL_COLLECTIONS_SYSTEM_ANNO,
|
|
"snowlCollectionsSystem",
|
|
0,
|
|
this.EXPIRE_NEVER);
|
|
// Ensure immediate children can't be removed.
|
|
PlacesUtils.bookmarks.setFolderReadonly(collsysID, true);
|
|
|
|
// Create sources collections folder.
|
|
itemID = PlacesUtils.bookmarks.
|
|
createFolder(collsysID,
|
|
strings.get("sourcesCollectionName"),
|
|
this.DEFAULT_INDEX);
|
|
PlacesUtils.annotations.
|
|
setItemAnnotation(itemID,
|
|
this.SNOWL_COLLECTIONS_ANNO,
|
|
"snowlCollectionsSources",
|
|
0,
|
|
this.EXPIRE_NEVER);
|
|
PlacesUtils.annotations.
|
|
setItemAnnotation(itemID,
|
|
this.SNOWL_PROPERTIES_ANNO,
|
|
"sysCollection",
|
|
0,
|
|
this.EXPIRE_NEVER);
|
|
// Ensure immediate children can't be removed.
|
|
PlacesUtils.bookmarks.setFolderReadonly(itemID, true);
|
|
|
|
// Create authors collections folder.
|
|
itemID = PlacesUtils.bookmarks.
|
|
createFolder(collsysID,
|
|
strings.get("authorsCollectionName"),
|
|
this.DEFAULT_INDEX);
|
|
PlacesUtils.annotations.
|
|
setItemAnnotation(itemID,
|
|
this.SNOWL_COLLECTIONS_ANNO,
|
|
"snowlCollectionsAuthors",
|
|
0,
|
|
this.EXPIRE_NEVER);
|
|
PlacesUtils.annotations.
|
|
setItemAnnotation(itemID,
|
|
this.SNOWL_PROPERTIES_ANNO,
|
|
"sysCollection",
|
|
0,
|
|
this.EXPIRE_NEVER);
|
|
// Ensure immediate children can't be removed.
|
|
PlacesUtils.bookmarks.setFolderReadonly(itemID, true);
|
|
|
|
// Default collections. These are folder shortcuts.
|
|
let coll, collections = [], viewItems, name;
|
|
// All Messages.
|
|
coll = {property: "sysCollection",
|
|
itemId: null,
|
|
value: "snowlCollectionsAll",
|
|
title: strings.get("allCollectionName"),
|
|
uri: URI("place:folder=" + collsysID + "&OR"),
|
|
anno: this.SNOWL_COLLECTIONS_ANNO,
|
|
parent: collsysID,
|
|
position: 0}; // 0=first
|
|
collections.push(coll);
|
|
|
|
// Build a map of all custom View folders and create the shortcuts,
|
|
// initially this will just include the Custom entry included as sample.
|
|
// XXX: in a Places rebuild scenario, order of View shortcuts will not be
|
|
// maintained, as the rebuild will happen from the order of the base
|
|
// folders - need to change order there if shortcuts dnd reordered.
|
|
viewItems = PlacesUtils.annotations
|
|
.getItemsWithAnnotation(this.SNOWL_USER_VIEW_ANNO, {});
|
|
for (var i=0; i < viewItems.length; i++) {
|
|
name = PlacesUtils.bookmarks.getItemTitle(viewItems[i]).split(":")[1];
|
|
this._log.info("init: Restoring User View - " + name + " - " + viewItems[i]);
|
|
coll = {property: "view",
|
|
itemId: null,
|
|
value: viewItems[i],
|
|
title: name,
|
|
uri: URI("place:folder=" + viewItems[i]),
|
|
anno: this.SNOWL_USER_VIEWLIST_ANNO,
|
|
parent: collsysID,
|
|
position: this.DEFAULT_INDEX};
|
|
collections.push(coll);
|
|
}
|
|
// Add the collections.
|
|
for each(let coll in collections) {
|
|
coll.itemId = PlacesUtils.bookmarks.insertBookmark(coll.parent,
|
|
coll.uri,
|
|
coll.position,
|
|
coll.title);
|
|
PlacesUtils.annotations.
|
|
setItemAnnotation(coll.itemId,
|
|
this.SNOWL_PROPERTIES_ANNO,
|
|
coll.property,
|
|
0,
|
|
this.EXPIRE_NEVER);
|
|
// This anno value must contain the itemId of the base folder if a
|
|
// View shortcut, otherwise string for AllMessages shortcut.
|
|
PlacesUtils.annotations.
|
|
setItemAnnotation(coll.itemId,
|
|
coll.anno,
|
|
coll.value,
|
|
0,
|
|
this.EXPIRE_NEVER);
|
|
};
|
|
|
|
PlacesUtils.bookmarks.insertSeparator(collsysID, 3);
|
|
|
|
// Build the map.
|
|
delete this.snowlPlacesQueries;
|
|
this.snowlPlacesQueries = this.buildNameItemMap(this.SNOWL_COLLECTIONS_ANNO);
|
|
// }
|
|
// };
|
|
|
|
// PlacesUtils.bookmarks.runInBatchMode(callback, null);
|
|
|
|
this.setPlacesVersion(snowlPlacesRoot);
|
|
this._placesInitialized = true;
|
|
|
|
// Set the system root itemId.
|
|
delete this.snowlPlacesFolderId;
|
|
return this.snowlPlacesFolderId = snowlPlacesRoot;
|
|
}
|
|
|
|
};
|
|
|
|
// FIXME: don't wrap statements in this wrapper for stable releases.
|
|
|
|
/**
|
|
* An implementation of mozIStorageStatementWrapper that logs execution times
|
|
* for debugging. Even though this implements an XPCOM interface, it isn't
|
|
* an XPCOM component, it's a regular JS object, so instead of instantiating it
|
|
* via createInstance, do |new InstrumentedStorageStatement()|.
|
|
*
|
|
* @param sqlString {string} the SQL string used to construct the statement
|
|
* (optional, but essential for useful debugging)
|
|
*/
|
|
function InstrumentedStorageStatement(sqlString, statement) {
|
|
this._sqlString = sqlString;
|
|
this._statement = Cc["@mozilla.org/storage/statement-wrapper;1"].
|
|
createInstance(Ci.mozIStorageStatementWrapper);
|
|
this._statement.initialize(statement);
|
|
//this._log = Log4Moz.repository.getLogger("Snowl.Statement");
|
|
}
|
|
|
|
InstrumentedStorageStatement.prototype = {
|
|
/**
|
|
* The SQL string used to construct the statement. We log this along with
|
|
* the execution time when the statement is executed.
|
|
*/
|
|
_sqlString: null,
|
|
|
|
/**
|
|
* The wrapped mozIStorageStatementWrapper (which itself wraps
|
|
* a mozIStorageStatement).
|
|
*/
|
|
_statement: null,
|
|
|
|
get _log() {
|
|
let log = Log4Moz.repository.getLogger("Snowl.Statement");
|
|
this.__defineGetter__("_log", function() log);
|
|
return this._log;
|
|
},
|
|
|
|
// nsISupports
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.mozIStorageStatementWrapper]),
|
|
|
|
// mozIStorageStatementWrapper
|
|
|
|
initialize: function() {},
|
|
|
|
get statement() { return this._statement.statement },
|
|
reset: function() { return this._statement.reset() },
|
|
|
|
step: function() {
|
|
// We don't want to log every step, just the first one, which triggers
|
|
// the execution of the query and is potentially slow.
|
|
let log = (this._statement.statement.state != Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_EXECUTING);
|
|
|
|
let before = new Date();
|
|
let rv = this._statement.step();
|
|
let after = new Date();
|
|
let time = after - before;
|
|
if (log)
|
|
this._log.trace(time + "ms to step initially " + this._sqlString);
|
|
return rv;
|
|
},
|
|
|
|
execute: function() {
|
|
let before = new Date();
|
|
let rv = this._statement.execute();
|
|
let after = new Date();
|
|
let time = after - before;
|
|
this._log.trace(time + "ms to execute " + this._sqlString);
|
|
return rv;
|
|
},
|
|
get row() { return this._statement.row },
|
|
get params() { return this._statement.params }
|
|
};
|
|
|
|
|
|
SnowlDatastore._dbInit();
|
|
|
|
// Intialize places
|
|
SnowlPlaces.init();
|