Merge Gloda changes to tip; these are currently not yet reviewed or turned on. Review will before this code is turned on by default.
Merge Gloda changes to tip; these are currently not yet reviewed or turned on. Review will before this code is turned on by default.
--- a/.hgtags
+++ b/.hgtags
@@ -3,8 +3,20 @@ 474f19a1b5fccec46bbeeeacd6f3cd368f3543a4
d7ce1e64f1cc52003e3f3f326bd8f5260ce03733 SEAMONKEY_2_0a1_BUILD1
d7ce1e64f1cc52003e3f3f326bd8f5260ce03733 SEAMONKEY_2_0a1_RELEASE
7fe8dc6fc848247b3df68fb7c6cbcd7c4665d497 THUNDERBIRD_3_0a3_RELEASE
7fe8dc6fc848247b3df68fb7c6cbcd7c4665d497 THUNDERBIRD_3_0a3_BUILD1
841a3d525cd73daac28e3d49b016bb46e6fcccbd THUNDERBIRD_3_0a3_RELEASE
841a3d525cd73daac28e3d49b016bb46e6fcccbd THUNDERBIRD_3_0a3_BUILD1
0000000000000000000000000000000000000000 THUNDERBIRD_3_0a3_RELEASE
0000000000000000000000000000000000000000 THUNDERBIRD_3_0a3_BUILD1
+93bb9e91d8cd4caa82602289230c380e10ceb3b1 gloda-milestone-0
+a3ff3e9b9edd130cebdf4ac48c7155265589a5d5 stable
+4b665f55dc96f043ac129be3f1c886546b5b9e6d gloda-milestone-1
+cb593cb6c3e57a01ac066c50362b37f8bdb2c2fc stable
+9f65871c06614dbec00b278cd60321713c01e028 stable
+c1902489c55f67799d41e7b032497ca04de6aa7c stable
+fe9ea5855aee1bfd0825edeaadc7547fd87504e4 stable
+8dfd8299e8a5f146d81040a844320e473e8a0fc2 stable
+3d52bc12f0f5d699a465432dc577cd479e12e0e7 stable-with-mods
+08c2e01d2acf8333311f497d050b22b01e72e460 stable-with-mods
+1936d4ec8044fd4cfdc636b43ec5e7488f86388c stable
+8e9fe32109b8a7232a5844320ca273bf5c3a4528 unstable
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/.project
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>gloda</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ </buildSpec>
+ <natures>
+ </natures>
+</projectDescription>
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/Makefile.in
@@ -0,0 +1,53 @@
+#
+# ***** 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 mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Mozilla Messaging, Inc.
+#
+# Portions created by the Initial Developer are Copyright (C) 2008
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of 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 *****
+
+DEPTH = ../../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+DIRS = modules components
+#DIRS = components
+
+ifdef ENABLE_TESTS
+DIRS += test
+endif
+
+include $(topsrcdir)/config/rules.mk
new file mode 100755
--- /dev/null
+++ b/mailnews/db/gloda/build.sh
@@ -0,0 +1,128 @@
+#!/bin/bash
+# build.sh -- builds JAR and XPI files for mozilla extensions
+# by Nickolay Ponomarev <asqueella@gmail.com>
+# (original version based on Nathan Yergler's build script)
+# Most recent version is at <http://kb.mozillazine.org/Bash_build_script>
+
+# This script assumes the following directory structure:
+# ./
+# chrome.manifest (optional - for newer extensions)
+# install.rdf
+# (other files listed in $ROOT_FILES)
+#
+# content/ |
+# locale/ |} these can be named arbitrary and listed in $CHROME_PROVIDERS
+# skin/ |
+#
+# defaults/ |
+# components/ |} these must be listed in $ROOT_DIRS in order to be packaged
+# ... |
+#
+# It uses a temporary directory ./build when building; don't use that!
+# Script's output is:
+# ./$APP_NAME.xpi
+# ./$APP_NAME.jar (only if $KEEP_JAR=1)
+# ./files -- the list of packaged files
+#
+# Note: It modifies chrome.manifest when packaging so that it points to
+# chrome/$APP_NAME.jar!/*
+
+#
+# default configuration file is ./config_build.sh, unless another file is
+# specified in command-line. Available config variables:
+APP_NAME= # short-name, jar and xpi files name. Must be lowercase with no spaces
+CHROME_PROVIDERS= # which chrome providers we have (space-separated list)
+CLEAN_UP= # delete the jar / "files" when done? (1/0)
+ROOT_FILES= # put these files in root of xpi (space separated list of leaf filenames)
+ROOT_DIRS= # ...and these directories (space separated list)
+BEFORE_BUILD= # run this before building (bash command)
+AFTER_BUILD= # ...and this after the build (bash command)
+
+if [ -z $1 ]; then
+ . ./config_build.sh
+else
+ . $1
+fi
+
+if [ -z $APP_NAME ]; then
+ echo "You need to create build config file first!"
+ echo "Read comments at the beginning of this script for more info."
+ exit;
+fi
+
+ROOT_DIR=`pwd`
+TMP_DIR=build
+
+#uncomment to debug
+#set -x
+
+# remove any left-over files from previous build
+rm -f $APP_NAME.jar $APP_NAME.xpi files
+rm -rf $TMP_DIR
+
+$BEFORE_BUILD
+
+mkdir --parents --verbose $TMP_DIR/chrome
+
+# generate the JAR file, excluding CVS and temporary files
+JAR_FILE=$TMP_DIR/chrome/$APP_NAME.jar
+echo "Generating $JAR_FILE..."
+for CHROME_SUBDIR in $CHROME_PROVIDERS; do
+ find $CHROME_SUBDIR -path '*CVS*' -prune -o -type f -print | grep -v \~ >> files
+done
+
+zip -0 -r $JAR_FILE `cat files`
+# The following statement should be used instead if you don't wish to use the JAR file
+#cp --verbose --parents `cat files` $TMP_DIR/chrome
+
+# prepare components and defaults
+echo "Copying various files to $TMP_DIR folder..."
+for DIR in $ROOT_DIRS; do
+ mkdir $TMP_DIR/$DIR
+ FILES="`find $DIR -path '*CVS*' -prune -o -type f -print | grep -v \~`"
+ echo $FILES >> files
+ cp --verbose --parents $FILES $TMP_DIR
+done
+
+# Copy other files to the root of future XPI.
+for ROOT_FILE in $ROOT_FILES install.rdf chrome.manifest; do
+ cp --verbose $ROOT_FILE $TMP_DIR
+ if [ -f $ROOT_FILE ]; then
+ echo $ROOT_FILE >> files
+ fi
+done
+
+cd $TMP_DIR
+
+if [ -f "chrome.manifest" ]; then
+ echo "Preprocessing chrome.manifest..."
+ # You think this is scary?
+ #s/^(content\s+\S*\s+)(\S*\/)$/\1jar:chrome\/$APP_NAME\.jar!\/\2/
+ #s/^(skin|locale)(\s+\S*\s+\S*\s+)(.*\/)$/\1\2jar:chrome\/$APP_NAME\.jar!\/\3/
+ #
+ # Then try this! (Same, but with characters escaped for bash :)
+ sed -i -r s/^\(content\\s+\\S*\\s+\)\(\\S*\\/\)$/\\1jar:chrome\\/$APP_NAME\\.jar!\\/\\2/ chrome.manifest
+ sed -i -r s/^\(skin\|locale\)\(\\s+\\S*\\s+\\S*\\s+\)\(.*\\/\)$/\\1\\2jar:chrome\\/$APP_NAME\\.jar!\\/\\3/ chrome.manifest
+
+ # (it simply adds jar:chrome/whatever.jar!/ at appropriate positions of chrome.manifest)
+fi
+
+# generate the XPI file
+echo "Generating $APP_NAME.xpi..."
+zip -r ../$APP_NAME.xpi *
+
+cd "$ROOT_DIR"
+
+echo "Cleanup..."
+if [ $CLEAN_UP = 0 ]; then
+ # save the jar file
+ mv $TMP_DIR/chrome/$APP_NAME.jar .
+else
+ rm ./files
+fi
+
+# remove the working files
+rm -rf $TMP_DIR
+echo "Done!"
+
+$AFTER_BUILD
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/chrome.manifest
@@ -0,0 +1,5 @@
+content gloda content/
+locale gloda en-US locale/en-US/
+skin gloda classic/1.0 skin/
+overlay chrome://messenger/content/messenger.xul chrome://gloda/content/thunderbirdOverlay.xul
+resource gloda ./
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/components/Makefile.in
@@ -0,0 +1,50 @@
+#
+# ***** 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 mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Mozilla Messaging, Inc.
+#
+# Portions created by the Initial Developer are Copyright (C) 2008
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of 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 *****
+
+DEPTH = ../../../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MODULE = mailnewsglobaldb
+
+EXTRA_COMPONENTS = $(wildcard $(srcdir)/*.js)
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/components/glautocomp.js
@@ -0,0 +1,494 @@
+/* ***** 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 Thunderbird Global Database.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Messaging, Inc.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Andrew Sutherland <asutherland@asutherland.org>
+ * Mark Banner <bugzilla@standard8.plus.com>
+ *
+ * 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 ***** */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+var LOG = null;
+
+var Gloda = null;
+var GlodaUtils = null;
+var MultiSuffixTree = null;
+var TagNoun = null;
+var FreeTagNoun = null;
+
+function ResultRowSingle(aItem, aCriteriaType, aCriteria, aExplicitNounID) {
+ this.nounID = aExplicitNounID || aItem.NOUN_ID;
+ this.nounDef = Gloda._nounIDToDef[this.nounID];
+ this.criteriaType = aCriteriaType;
+ this.criteria = aCriteria;
+ this.item = aItem;
+}
+ResultRowSingle.prototype = {
+ multi: false
+};
+
+function ResultRowMulti(aNounID, aCriteriaType, aCriteria, aQuery) {
+ this.nounID = aNounID;
+ this.nounDef = Gloda._nounIDToDef[aNounID];
+ this.criteriaType = aCriteriaType;
+ this.criteria = aCriteria;
+ this.collection = aQuery.getCollection(this);
+ this.renderer = null;
+}
+ResultRowMulti.prototype = {
+ multi: true,
+ onItemsAdded: function(aItems) {
+ LOG.debug("RRM onItemsAdded: " + aItems.length + ": " + aItems);
+ if (this.renderer) {
+ LOG.debug("RRM rendering...");
+ for each (let [iItem, item] in Iterator(aItems)) {
+ LOG.debug("RRM ..." + item);
+ this.renderer.renderItem(item);
+ }
+ }
+ },
+ onItemsModified: function(aItems) {
+ },
+ onItemsRemoved: function(aItems) {
+ },
+ onQueryCompleted: function() {
+ }
+}
+
+function nsAutoCompleteGlodaResult(aListener, aCompleter, aString) {
+ this.listener = aListener;
+ this.completer = aCompleter;
+ this.searchString = aString;
+ this._results = [];
+ this._pendingCount = 0;
+ this._problem = false;
+
+ this.wrappedJSObject = this;
+}
+nsAutoCompleteGlodaResult.prototype = {
+ getObjectAt: function(aIndex) {
+ return this._results[aIndex];
+ },
+ markPending: function ACGR_markPending(aCompleter) {
+ this._pendingCount++;
+ },
+ markCompleted: function ACGR_markCompleted(aCompleter) {
+ if (--this._pendingCount == 0) {
+ LOG.debug("Notifying completion.");
+ this.listener.onSearchResult(this.completer, this);
+ }
+ },
+ addRows: function ACGR_addRows(aRows) {
+ if (!aRows.length)
+ return;
+ LOG.debug("Adding " + aRows.length + " rows (" + this._pendingCount +
+ " jobs still pending)");
+ this._results.push.apply(this._results, aRows);
+ this.listener.onSearchResult(this.completer, this);
+ },
+ // ==== nsIAutoCompleteResult
+ searchString: null,
+ get searchResult() {
+ if (this._problem)
+ return Ci.nsIAutoCompleteResult.RESULT_FAILURE;
+ if (this._results.length)
+ return (!this._pendingCount) ? Ci.nsIAutoCompleteResult.RESULT_SUCCESS
+ : Ci.nsIAutoCompleteResult.RESULT_SUCCESS_ONGOING;
+ else
+ return (!this._pendingCount) ? Ci.nsIAutoCompleteResult.RESULT_NOMATCH
+ : Ci.nsIAutoCompleteResult.RESULT_NOMATCH_ONGOING;
+ },
+ defaultIndex: -1,
+ errorDescription: null,
+ get matchCount() {
+ return (this._results === null) ? 0 : this._results.length;
+ },
+ // this is the lower text, (shows the url in firefox)
+ // we try and show the contact's name here.
+ getValueAt: function(aIndex) {
+ let thing = this._results[aIndex];
+ return thing.name || thing.value || thing.subject;
+ },
+ // rich uses this to be the "title". it is the upper text
+ // we try and show the identity here.
+ getCommentAt: function(aIndex) {
+ let thing = this._results[aIndex];
+ if (thing.value) // identity
+ return thing.contact.name;
+ else
+ return thing.name || thing.subject;
+ },
+ // rich uses this to be the "type"
+ getStyleAt: function(aIndex) {
+ let row = this._results[aIndex];
+ if (row.multi)
+ return "gloda-multi";
+ else
+ return "gloda-single-" + row.nounDef.name;
+ },
+ // rich uses this to be the icon
+ getImageAt: function(aIndex) {
+ let thing = this._results[aIndex];
+ if (!thing.value)
+ return null;
+
+ let md5hash = GlodaUtils.md5HashString(thing.value);
+ let gravURL = "http://www.gravatar.com/avatar/" + md5hash +
+ "?d=identicon&s=32&r=g";
+ return gravURL;
+ },
+ removeValueAt: function() {},
+
+ _stop: function() {
+ },
+};
+
+const MAX_POPULAR_CONTACTS = 200;
+
+/**
+ * Complete contacts/identities based on name/email. Instant phase is based on
+ * a suffix-tree built of popular contacts/identities. Delayed phase relies
+ * on a LIKE search of all known contacts.
+ */
+function ContactIdentityCompleter() {
+ // get all the contacts
+ let contactQuery = Gloda.newQuery(Gloda.NOUN_CONTACT);
+ contactQuery.orderBy("-popularity").limit(MAX_POPULAR_CONTACTS);
+ this.contactCollection = contactQuery.getCollection(this, null);
+}
+ContactIdentityCompleter.prototype = {
+ _popularitySorter: function(a, b){ return b.popularity - a.popularity; },
+ complete: function ContactIdentityCompleter_complete(aResult, aString) {
+ if (aString.length < 3)
+ return false;
+
+ let matches;
+ if (this.suffixTree) {
+ matches = this.suffixTree.findMatches(aString.toLowerCase());
+ LOG.debug("CIC: Suffix Tree found " + matches.length + " matches.")
+ }
+ else
+ matches = [];
+
+ // let's filter out duplicates due to identity/contact double-hits by
+ // establishing a map based on the contact id for these guys.
+ // let's also favor identities as we do it, because that gets us the
+ // most accurate gravat, potentially
+ let contactToThing = {};
+ for (let iMatch = 0; iMatch < matches.length; iMatch++) {
+ let thing = matches[iMatch];
+ if (thing.NOUN_ID == Gloda.NOUN_CONTACT && !(thing.id in contactToThing))
+ contactToThing[thing.id] = thing;
+ else if (thing.NOUN_ID == Gloda.NOUN_IDENTITY)
+ contactToThing[thing.contactID] = thing;
+ }
+ // and since we can now map from contacts down to identities, map contacts
+ // to the first identity for them that we find...
+ matches = [val.NOUN_ID == Gloda.NOUN_IDENTITY ? val : val.identities[0]
+ for each ([iVal, val] in Iterator(contactToThing))];
+
+ let rows = [new ResultRowSingle(match, "text", aResult.searchString)
+ for each ([iMatch, match] in Iterator(matches))];
+ aResult.addRows(rows);
+
+ // - match against database contacts / identities
+ let pending = {contactToThing: contactToThing, pendingCount: 2};
+
+ LOG.debug("CIC: issuing contact LIKE query");
+ let contactQuery = Gloda.newQuery(Gloda.NOUN_CONTACT);
+ contactQuery.nameLike(contactQuery.WILD, aString, contactQuery.WILD);
+ pending.contactColl = contactQuery.getCollection(this, aResult);
+
+ LOG.debug("CIC: issuing identity LIKE query");
+ let identityQuery = Gloda.newQuery(Gloda.NOUN_IDENTITY);
+ identityQuery.kind("email").valueLike(identityQuery.WILD, aString,
+ identityQuery.WILD);
+ pending.identityColl = identityQuery.getCollection(this, aResult);
+
+ aResult._contactCompleterPending = pending;
+
+ return true;
+ },
+ onItemsAdded: function(aItems, aCollection) {
+ },
+ onItemsModified: function(aItems, aCollection) {
+ },
+ onItemsRemoved: function(aItems, aCollection) {
+ },
+ onQueryCompleted: function(aCollection) {
+ // handle the initial setup case...
+ if (aCollection.data == null) {
+ LOG.debug("CIC: Initial query found " + aCollection.items.length);
+ // cheat and explicitly add our own contact...
+ if (!(Gloda.myContact.id in this.contactCollection._idMap))
+ this.contactCollection._onItemsAdded([Gloda.myContact]);
+
+ // the set of identities owned by the contacts is automatically loaded as part
+ // of the contact loading...
+ // (but only if we actually have any contacts)
+ this.identityCollection =
+ this.contactCollection.subCollections[Gloda.NOUN_IDENTITY];
+
+ let contactNames = [(c.name.replace(" ", "").toLowerCase() || "x") for each
+ ([, c] in Iterator(this.contactCollection.items))];
+ // if we had no contacts, we will have no identity collection!
+ let identityMails;
+ if (this.identityCollection)
+ identityMails = [i.value.toLowerCase() for each
+ ([, i] in Iterator(this.identityCollection.items))];
+
+ this.suffixTree = new MultiSuffixTree(contactNames.concat(identityMails),
+ this.contactCollection.items.concat(this.identityCollection.items));
+
+ return;
+ }
+
+ LOG.debug("CIC: LIKE query found " + aCollection.items.length);
+
+ // handle the completion case
+ let result = aCollection.data;
+ let pending = result._contactCompleterPending;
+
+ if (--pending.pendingCount == 0) {
+ let possibleDudes = [];
+
+ let contactToThing = pending.contactToThing;
+
+ let items;
+
+ // check identities first because they are better than contacts in terms
+ // of display
+ items = pending.identityColl.items;
+ for (let iIdentity = 0; iIdentity < items.length; iIdentity++){
+ let identity = items[iIdentity];
+ if (!(identity.contactID in contactToThing)) {
+ contactToThing[identity.contactID] = identity;
+ possibleDudes.push(identity);
+ // augment the identity with its contact's popularity
+ identity.popularity = identity.contact.popularity;
+ }
+ }
+ items = pending.contactColl.items;
+ for (let iContact = 0; iContact < items.length; iContact++) {
+ let contact = items[iContact];
+ if (!(contact.id in contactToThing)) {
+ contactToThing[contact.id] = contact;
+ possibleDudes.push(contact.identities[0]);
+ }
+ }
+
+ // sort in order of descending popularity
+ possibleDudes.sort(this._popularitySorter);
+ let rows = [new ResultRowSingle(dude, "text", result.searchString)
+ for each ([iDude, dude] in Iterator(possibleDudes))];
+ result.addRows(rows);
+ result.markCompleted(this);
+
+ // the collections no longer care about the result, make it clear.
+ delete pending.identityColl.data;
+ delete pending.contactColl.data;
+ // the result object no longer needs us or our data
+ delete result._contactCompleterPending;
+ }
+ else {
+ LOG.debug("ignoring... pending is still: " + pending.pendingCount);
+ }
+ }
+};
+
+/**
+ * Complete tags that are used on contacts.
+ */
+function ContactTagCompleter() {
+ FreeTagNoun.populateKnownFreeTags();
+ this._buildSuffixTree();
+ FreeTagNoun.addListener(this);
+}
+ContactTagCompleter.prototype = {
+ _buildSuffixTree: function() {
+ let tagNames = [], tags = [];
+ for (let [tagName, tag] in Iterator(FreeTagNoun.knownFreeTags)) {
+ tagNames.push(tagName.toLowerCase());
+ tags.push(tag);
+ LOG.debug("contact tag: " + tagName);
+ }
+ this._suffixTree = new MultiSuffixTree(tagNames, tags);
+ this._suffixTreeDirty = false;
+ },
+ onFreeTagAdded: function(aTag) {
+ this._suffixTreeDirty = true;
+ },
+ complete: function ContactTagCompleter_complete(aResult, aString) {
+ // now is not the best time to do this; have onFreeTagAdded use a timer.
+ if (this._suffixTreeDirty)
+ this._buildSuffixTree();
+
+ if (aString.length < 2)
+ return false; // no async mechanism that will add new rows
+
+ LOG.debug("Completing on contact tags...");
+
+ tags = this._suffixTree.findMatches(aString.toLowerCase());
+ let rows = [];
+ for each (let [iTag, tag] in Iterator(tags)) {
+ let query = Gloda.newQuery(Gloda.NOUN_CONTACT);
+ LOG.debug(" checking for contact tag: " + tag.name);
+ query.freeTags(tag);
+ let resRow = new ResultRowMulti(Gloda.NOUN_CONTACT, "tag", tag.name,
+ query);
+ rows.push(resRow);
+ }
+ aResult.addRows(rows);
+
+ return false; // no async mechanism that will add new rows
+ }
+};
+
+/**
+ * Complete tags that are used on messages
+ */
+function MessageTagCompleter() {
+ this._buildSuffixTree();
+}
+MessageTagCompleter.prototype = {
+ _buildSuffixTree: function MessageTagCompleter__buildSufficeTree() {
+ let tagNames = [], tags = [];
+ let tagArray = TagNoun.getAllTags();
+ for (let iTag = 0; iTag < tagArray.length; iTag++) {
+ let tag = tagArray[iTag];
+ tagNames.push(tag.tag.toLowerCase());
+ tags.push(tag);
+ LOG.debug("message tag: " + tag.tag);
+ }
+ this._suffixTree = new MultiSuffixTree(tagNames, tags);
+ this._suffixTreeDirty = false;
+ },
+ complete: function MessageTagCompleter_complete(aResult, aString) {
+ if (aString.length < 2)
+ return false;
+
+ LOG.debug("Completing on message tags...");
+
+ tags = this._suffixTree.findMatches(aString.toLowerCase());
+ let rows = [];
+ for each (let [, tag] in Iterator(tags)) {
+ LOG.debug(" found message tag: " + tag.tag);
+ let resRow = new ResultRowSingle(tag, "tag", tag.tag, TagNoun.id);
+ rows.push(resRow);
+ }
+ aResult.addRows(rows);
+
+ return false; // no async mechanism that will add new rows
+ }
+};
+
+function nsAutoCompleteGloda() {
+ this.wrappedJSObject = this;
+
+ // set up our awesome globals!
+ if (Gloda === null) {
+ let loadNS = {};
+
+ Cu.import("resource://gloda/modules/public.js", loadNS);
+ Gloda = loadNS.Gloda;
+
+ Cu.import("resource://gloda/modules/utils.js", loadNS);
+ GlodaUtils = loadNS.GlodaUtils;
+ Cu.import("resource://gloda/modules/suffixtree.js", loadNS);
+ MultiSuffixTree = loadNS.MultiSuffixTree;
+ Cu.import("resource://gloda/modules/noun_tag.js", loadNS);
+ TagNoun = loadNS.TagNoun;
+ Cu.import("resource://gloda/modules/noun_freetag.js", loadNS);
+ FreeTagNoun = loadNS.FreeTagNoun;
+
+ Cu.import("resource://gloda/modules/log4moz.js", loadNS);
+ LOG = loadNS["Log4Moz"].Service.getLogger("gloda.autocomp");
+ }
+
+ LOG.debug("initializing completers");
+
+ this.completers = [];
+
+ this.curResult = null;
+
+dump("init CIC\n");
+ LOG.debug("initializing ContactIdentityCompleter");
+ try {
+ this.completers.push(new ContactIdentityCompleter());
+ } catch (ex) {dump("CICEX: " + ex.fileName + ":" + ex.lineNumber + ": " + ex);}
+dump("init CTC\n");
+ LOG.debug("initializing ContactTagCompleter");
+ this.completers.push(new ContactTagCompleter());
+dump("init MTC\n");
+ LOG.debug("initializing MessageTagCompleter");
+ try {
+ this.completers.push(new MessageTagCompleter());
+ } catch (ex) {dump("MTCEX: " + ex.fileName + ":" + ex.lineNumber + ": " + ex);}
+
+ LOG.debug("initialized completers");
+}
+
+nsAutoCompleteGloda.prototype = {
+ classDescription: "AutoCompleteGloda",
+ contractID: "@mozilla.org/autocomplete/search;1?name=gloda",
+ classID: Components.ID("{3bbe4d77-3f70-4252-9500-bc00c26f476c}"),
+ QueryInterface: XPCOMUtils.generateQI([
+ Components.interfaces.nsIAutoCompleteSearch]),
+
+ startSearch: function(aString, aParam, aResult, aListener) {
+ let result = new nsAutoCompleteGlodaResult(aListener, this, aString);
+ // save this for hacky access to the search. I somewhat suspect we simply
+ // should not be using the formal autocomplete mechanism at all.
+ this.curResult = result;
+
+ for each (let [iCompleter, completer] in Iterator(this.completers)) {
+ // they will return true if they have something pending.
+ if (completer.complete(result, aString))
+ result.markPending(completer);
+ }
+
+ aListener.onSearchResult(this, result);
+ },
+
+ stopSearch: function() {
+ },
+};
+
+function NSGetModule(compMgr, fileSpec) {
+ return XPCOMUtils.generateModule([nsAutoCompleteGloda]);
+}
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/components/jsmimeemitter.js
@@ -0,0 +1,456 @@
+/* ***** 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 Thunderbird Global Database.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Messaging, Inc.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Andrew Sutherland <asutherland@asutherland.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 ***** */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const kStateUnknown = 0;
+const kStateInHeaders = 1;
+const kStateInBody = 2;
+const kStateInAttachment = 3;
+
+/**
+ * Custom nsIMimeEmitter to build a sub-optimal javascript representation of a
+ * MIME message. The intent is that a better mechanism than is evolved to
+ * provide a javascript-accessible representation of the message.
+ *
+ * Processing occurs in two passes. During the first pass, libmime is parsing
+ * the stream it is receiving, and generating header and body events for all
+ * MimeMessage instances it encounters. This provides us with the knowledge
+ * of each nested message in addition to the top level message, their headers
+ * and sort-of their bodies. The sort-of is that we may get more than
+ * would normally be displayed in cases involving multipart/alternatives.
+ * During the second pass, the libmime object model is traversed, generating
+ * attachment notifications for all leaf nodes. From our perspective, this
+ * means file attachments and embedded messages (message/rfc822). We use this
+ * pass to create the attachment objects and properly structure the MIME part
+ * hierarchy. We extract the 'part name' (ex: 1.2.2.1) from the URL provided
+ * with the attacment and rely on the fact that the attachment notifications
+ * are generated as the result of an in-order traversal of the hierarchy. We
+ * generate MimeUnknown instances for apparent leaf nodes (nodes for whom
+ * we did not hear about and do not know of any of their children), and
+ * MimeContainer instances for apparent container nodes (nodes for whom we
+ * know about one or more children).
+ */
+function MimeMessageEmitter() {
+ this._mimeMsg = {};
+ Cu.import("resource://gloda/modules/mimemsg.js", this._mimeMsg);
+
+ this._url = null;
+ this._channel = null;
+
+ this._inputStream = null;
+ this._outputStream = null;
+
+ this._outputListener = null;
+
+ this._rootMsg = null;
+ this._messageStack = [];
+ this._parentMsg = null;
+ this._curMsg = null;
+
+ this._messageIndex = 0;
+ this._allSubMessages = [];
+
+ this._partMap = {};
+ this._curPart = null;
+ this._curBodyPart = null;
+
+ this._state = kStateUnknown;
+}
+
+MimeMessageEmitter.prototype = {
+ classDescription: "JS Mime Message Emitter",
+ classID: Components.ID("{8cddbbbc-7ced-46b0-a936-8cddd1928c24}"),
+ contractID: "@mozilla.org/gloda/jsmimeemitter;1",
+
+ _partRE: new RegExp("^[^?]+\?(?:[^&]+&)*part=([^&]+)(?:&[^&]+)*$"),
+
+ _xpcom_categories: [{
+ category: "mime-emitter",
+ entry:
+ "@mozilla.org/messenger/mimeemitter;1?type=application/x-js-mime-message",
+ }],
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIMimeEmitter]),
+
+ initialize: function mime_emitter_initialize(aUrl, aChannel, aFormat) {
+ this._url = aUrl;
+ this._curMsg = this._parentMsg = this._rootMsg = new this._mimeMsg.MimeMessage();
+ this._curMsg.partName = "";
+ this._partMap[""] = this._curMsg;
+
+ this._mimeMsg.MsgHdrToMimeMessage.RESULT_RENDEVOUZ[aUrl.spec] =
+ this._rootMsg;
+
+ this._channel = aChannel;
+ },
+
+ complete: function mime_emitter_complete() {
+ // dump("!!!!\n!!!!\n!!!!\n" + this._rootMsg.prettyString() + "\n");
+ this._url = null;
+ this._channel = null;
+
+ this._inputStream = null;
+ this._outputStream = null;
+
+ this._outputListener = null;
+
+ this._curMsg = this._parentMsg = this._messageStack = this._rootMsg = null;
+ this._messageIndex = null;
+ this._allSubMessages = null;
+
+ this._partMap = null;
+ this._curPart = null;
+ this._curBodyPart = null;
+ },
+
+ setPipe: function mime_emitter_setPipe(aInputStream, aOutputStream) {
+ this._inputStream = aInputStream;
+ this._outputStream = aOutputStream;
+ },
+ set outputListener(aListener) {
+ this._outputListener = aListener;
+ },
+ get outputListener() {
+ return this._outputListener;
+ },
+
+ _beginPayload: function mime_emitter__beginPayload(aContentType, aIsPart) {
+ aContentType = aContentType.toLowerCase();
+ if (aContentType == "text/plain" || aContentType == "text/html") {
+ this._curBodyPart = new this._mimeMsg.MimeBody(aContentType, aIsPart);
+ this._parentMsg.bodyParts.push(this._curBodyPart);
+ this._curPart = aIsPart ? this._curBodyPart : null;
+ }
+ else if (aContentType == "message/rfc822") {
+ // startBody will take care of this
+ this._curPart = this._curBodyPart = null;
+ }
+ // this is going to fall-down with TNEF encapsulation and such, we really
+ // need to just be consuming the object model.
+ else if (aContentType.indexOf("multipart/") == 0) {
+ this._curBodyPart = null;
+ // alternatives are always parts for part numbering purposes
+ this._curPart = aIsPart ? new this._mimeMsg.MimeContainer(aContentType)
+ : null;
+ }
+ else {
+ this._curBodyPart = null;
+ this._curPart = aIsPart ?
+ new this._mimeMsg.MimeUnknown(aContentType, aIsPart) : null;
+ }
+ },
+
+ // ----- Header Routines
+ startHeader: function mime_emitter_startHeader(aIsRootMailHeader,
+ aIsHeaderOnly, aMsgID, aOutputCharset) {
+ this._state = kStateInHeaders;
+ if (aIsRootMailHeader) {
+ this.updateCharacterSet(aOutputCharset);
+ // nothing to do curMsg-wise, already initialized.
+ }
+ else {
+ this._curMsg = new this._mimeMsg.MimeMessage();
+
+ this._curMsg.partName = this._savedPartPath;
+ this._placePart(this._curMsg);
+ delete this._savedPartPath;
+
+ this._parentMsg.messages.push(this._curMsg);
+ this._allSubMessages.push(this._curMsg);
+ }
+ },
+ addHeaderField: function mime_emitter_addHeaderField(aField, aValue) {
+ if (this._state == kStateInBody) {
+ aField = aField.toLowerCase();
+ let indexSemi = aValue.indexOf(";");
+ if (indexSemi >= 0)
+ aValue = aValue.substring(0, indexSemi);
+ if (aField == "content-type")
+ this._beginPayload(aValue, true);
+ else if (aField == "x-jsemitter-part-path") {
+ if (this._curPart) {
+ this._curPart.partName = aValue;
+ this._placePart(this._curPart);
+ }
+ else
+ this._savedPartPath = aValue;
+ }
+ return;
+ }
+ if (this._state != kStateInHeaders)
+ return;
+ let lowerField = aField.toLowerCase();
+ if (lowerField in this._curMsg.headers)
+ this._curMsg.headers[lowerField].push(aValue);
+ else
+ this._curMsg.headers[lowerField] = [aValue];
+ },
+ addAllHeaders: function mime_emitter_addAllHeaders(aAllHeaders, aHeaderSize) {
+ // This is called by the parsing code after the calls to AddHeaderField (or
+ // AddAttachmentField if the part is an attachment), and seems to serve
+ // a specialized, quasi-redundant purpose. (nsMimeBaseEmitter creates a
+ // nsIMimeHeaders instance and hands it to the nsIMsgMailNewsUrl.)
+ // nop
+ },
+ writeHTMLHeaders: function mime_emitter_writeHTMLHeaders() {
+ // It does't look like this should even be part of the interface; I think
+ // only the nsMimeHtmlDisplayEmitter::EndHeader call calls this signature.
+ // nop
+ },
+ endHeader: function mime_emitter_endHeader() {
+ },
+ updateCharacterSet: function mime_emitter_updateCharacterSet(aCharset) {
+ // for non US-ASCII, ISO-8859-1, or UTF-8 charsets (case-insensitive),
+ // nsMimeBaseEmitter grabs the channel's content type, nukes the "charset="
+ // parameter if it exists, and tells the channel the updated content type
+ // and new character set.
+
+ // Disabling for now; we get a NS_ERROR_NOT_IMPLEMENTED from the channel
+ // when we try and set the contentCharset... and I'm not totally up on the
+ // intent of why we were doing this in the first place.
+ /*
+ let upperCharset = aCharset.toUpperCase();
+
+ if ((upperCharset != "US-ASCII") && (upperCharset != "ISO-8859-1") &&
+ (upperCharset != "UTF-8")) {
+
+ let curContentType = this._channel.contentType;
+ let charsetIndex = curContentType.toLowerCase().indexOf("charset=");
+ if (charsetIndex >= 0) {
+ // assume a space or semicolon delimits
+ curContentType = curContentType.substring(0, charsetIndex-1);
+ }
+
+ this._channel.contentType = curContentType;
+ this._channel.contentCharset = aCharset;
+ }
+ */
+ },
+
+ /**
+ * Place a part in its proper location. We know that we are called as a
+ * result of in-order traversal, so this is wildly easy to deal with.
+ */
+ _placePart: function(aPart) {
+ let partName = aPart.partName;
+ this._partMap[partName] = aPart;
+ let parentName = partName.substring(0, partName.lastIndexOf("."));
+ let parentPart = this._partMap[parentName];
+ parentPart.parts.push(aPart);
+ },
+
+ /**
+ * In the case of attachments, we need to replace an existing part with a
+ * more representative part...
+ */
+ _replacePart: function(aPart) {
+ let partName = aPart.partName;
+ this._partMap[partName] = aPart;
+
+ let parentName = partName.substring(0, partName.lastIndexOf("."));
+ let parentPart = this._partMap[parentName];
+
+ let childNamePart = partName.substring(partName.lastIndexOf(".")+1);
+ let childIndex = parseInt(childNamePart) - 1;
+
+ let oldPart = parentPart.parts[childIndex];
+ parentPart.parts[childIndex] = aPart;
+ aPart.parts = oldPart.parts;
+
+ // - remove it if it was a body part. This can happen for text/plain
+ // attachments. Like patches.
+ // (climb the parents until we find a message/bodyparts holder...)
+ while (parentPart.partName && !parentPart.bodyParts) {
+ parentName = parentName.substring(0, parentName.lastIndexOf("."));
+ parentPart = this._partMap[parentName];
+ }
+ if (parentPart.bodyParts && parentPart.bodyParts.indexOf(oldPart) >= 0)
+ parentPart.bodyParts.splice(parentPart.bodyParts.indexOf(oldPart), 1);
+ },
+
+ /**
+ * Put a part at its proper location. We rely on this method to be called
+ * in the the sequence generated by StartAttachment (an in-order traversal
+ * of the MIME structure).
+ */
+ _putPart: function(aPartPath, aPathSoFar, aPart, aParent) {
+ let dotIndex = aPartPath.indexOf(".");
+ let curPath, remPath;
+ if (dotIndex >= 0) {
+ curPath = aPartPath.substring(0, dotIndex);
+ remPath = aPartPath.substring(dotIndex+1);
+ }
+ else {
+ curPath = aPartPath;
+ remPath = null;
+ }
+ let newPathSoFar = aPathSoFar + "." + curPath;
+ let curIndex = parseInt(curPath) - 1;
+
+ // for parts that should exist, try and find them in the part map, otherwise
+ // create MimeUnknowns
+ while (curIndex > aParent.parts.length) {
+ let tempPath = aPathSoFar + "." + aParent.parts.length;
+ if (tempPath in this._partMap)
+ aParent.parts.push(this._partMap[tempPath]);
+ else {
+ let newPart = new this._mimeMsg.MimeUnknown("unknown/unknown", true);
+ newPart.partName = tempPath;
+ aParent.parts.push(newPart);
+ }
+ }
+
+ // are we a leaf?
+ if (remPath !== null) {
+ // no, we are not a leaf
+ if (curIndex == aParent.parts.length) {
+ // and we need to add a container
+ if (newPathSoFar in this._partMap)
+ aParent.parts.push(this._partMap[newPathSoFar]);
+ else {
+ let newPart = new this._mimeMsg.MimeContainer("multipart/unknown",
+ true);
+ newPart.partName = newPathSoFar;
+ aParent.parts.push(newPart);
+ }
+ }
+ this._putPart(remPath, newPathSoFar, aPart, aParent.parts[curIndex]);
+ }
+ else {
+ // yes, we are a leaf, we just go here...
+ aParent.parts.push(aPart);
+ }
+ },
+
+ // ----- Attachment Routines
+ // The attachment processing happens after the initial streaming phase (during
+ // which time we receive the messages, both bodies and headers). Our caller
+ // traverses the libmime child object hierarchy, emitting an attachment for
+ // each leaf object or sub-message.
+ startAttachment: function mime_emitter_startAttachment(aName, aContentType,
+ aUrl, aNotDownloaded) {
+ this._state = kStateInAttachment;
+ // we need to strip our magic flags from the URL
+ aURl = aUrl.replace("header=filter&emitter=js&", "");
+
+ // the url should contain a part= piece that tells us the part name, which
+ // we then use to figure out where.
+ let partMatch = this._partRE.exec(aUrl);
+ let partName = partMatch[1];
+
+ let part;
+ if (aContentType == "message/rfc822") {
+ // we already have all we need to know about the message, ignore it
+ return;
+ }
+ else {
+ // create the attachment
+ part = new this._mimeMsg.MimeMessageAttachment(partName,
+ aName, aContentType, aUrl, aNotDownloaded);
+ }
+
+ if (part.isRealAttachment) {
+ // replace the existing part with the attachment...
+ this._replacePart(part);
+ }
+ },
+ addAttachmentField: function mime_emitter_addAttachmentField(aField, aValue) {
+ // this only gives us X-Mozilla-PartURL, which is the same as aUrl we
+ // already got previously, so need to do anything with this.
+ },
+ endAttachment: function mime_emitter_endAttachment() {
+ // don't need to do anything here, since we don't care about the headers.
+ },
+ endAllAttachments: function mime_emitter_endAllAttachments() {
+ // nop
+ },
+
+ // ----- Body Routines
+ startBody: function mime_emitter_startBody(aIsBodyOnly, aMsgID, aOutCharset) {
+ this._state = kStateInBody;
+
+ this._messageStack.push(this._curMsg);
+ this._parentMsg = this._curMsg;
+
+ // begin payload processing
+ let contentType = this._curMsg.get("content-type", "text/plain");
+ let indexSemi = contentType.indexOf(";");
+ if (indexSemi >= 0)
+ contentType = contentType.substring(0, indexSemi);
+ this._beginPayload(contentType, true);
+ if (this._parentMsg.partName == "")
+ this._curPart.partName = "1";
+ else
+ this._curPart.partName = this._curMsg.partName + ".1";
+ this._placePart(this._curPart);
+ },
+
+ writeBody: function mime_emitter_writeBody(aBuf, aSize, aOutAmountWritten) {
+ if (this._curBodyPart)
+ this._curBodyPart.body += aBuf;
+ },
+
+ endBody: function mime_emitter_endBody() {
+ this._messageStack.pop();
+ this._parentMsg = this._messageStack[this._messageStack.length - 1];
+ },
+
+ // ----- Generic Write (confusing)
+ // (binary data writing...)
+ write: function mime_emitter_write(aBuf, aSize, aOutAmountWritten) {
+ // we don't actually ever get called because we don't have the attachment
+ // binary payloads pass through us, but we do the following just in case
+ // we did get called (otherwise the caller gets mad and throws exceptions).
+ aOutAmountWritten.value = aSize;
+ },
+
+ // (string writing)
+ utilityWrite: function mime_emitter_utilityWrite(aBuf) {
+ this.write(aBuf, aBuf.length, {});
+ },
+};
+
+var components = [MimeMessageEmitter];
+function NSGetModule(compMgr, fileSpec) {
+ return XPCOMUtils.generateModule(components);
+}
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/config_build.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+# Build config for build.sh
+APP_NAME=gloda
+CHROME_PROVIDERS="content locale skin"
+CLEAN_UP=1
+ROOT_FILES=
+ROOT_DIRS="defaults modules"
+BEFORE_BUILD=
+AFTER_BUILD=
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/content/glodacomplete.css
@@ -0,0 +1,42 @@
+textbox[type="glodacomplete"] {
+ -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete");
+}
+
+panel[type="glodacomplete-richlistbox"] {
+ -moz-binding: url("chrome://gloda/content/glodacomplete.xml#glodacomplete-rich-result-popup");
+}
+
+.autocomplete-richlistbox {
+ -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistbox");
+ -moz-user-focus: ignore;
+ -moz-appearance: none;
+}
+
+.autocomplete-richlistbox > scrollbox {
+ overflow-x: hidden !important;
+}
+
+.autocomplete-richlistitem[type="gloda-single-tag"] {
+ -moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-single-tag-item");
+ overflow: -moz-hidden-unscrollable;
+}
+
+.autocomplete-richlistitem[type="gloda-single-identity"] {
+ -moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-single-identity-item");
+ -moz-box-orient: vertical;
+ overflow: -moz-hidden-unscrollable;
+}
+
+richlistitem[type="gloda-contact-chunk"] {
+ -moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-contact-chunk");
+ -moz-box-orient: vertical;
+ overflow: -moz-hidden-unscrollable;
+}
+
+.autocomplete-richlistitem[type="gloda-multi"] {
+ -moz-binding: url("chrome://gloda/content/glodacomplete.xml#gloda-multi-item");
+ -moz-box-orient: vertical;
+ overflow: -moz-hidden-unscrollable;
+}
+
+/* .autocomplete-history-dropmarker wants to be optional, but we don't care */
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/content/glodacomplete.xml
@@ -0,0 +1,624 @@
+<?xml version="1.0"?>
+
+<!-- ***** 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 mozilla.org browser.
+ -
+ - The Initial Developer of the Original Code is
+ - Joe Hewitt.
+ - Portions created by the Initial Developer are Copyright (C) 2003
+ - the Initial Developer. All Rights Reserved.
+ -
+ - Contributor(s):
+ - Pierre Chanial (p_ch@verizon.net)
+ - Dean Tessman (dean_tessman@hotmail.com)
+ - Masayuki Nakano (masayuki@d-toybox.com)
+ - Pamela Greene (pamg.bugs@gmail.com)
+ - Edward Lee (edward.lee@engineering.uiuc.edu)
+ - Andrew Sutherland (asutherland@asutherland.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 LGPL or the GPL. 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 ***** -->
+
+<bindings id="autocompleteBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="glodacomplete-rich-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup">
+ <implementation implements="nsIAutoCompletePopup">
+ <method name="_appendCurrentResult">
+ <body>
+ <![CDATA[
+ var controller = this.mInput.controller;
+
+ // Process maxRows per chunk to improve performance and user experience
+ for (let i = 0; i < this.maxRows; i++) {
+ if (this._currentIndex >= this._matchCount)
+ return;
+
+ var existingItemsCount = this.richlistbox.childNodes.length;
+ var item;
+
+ // trim the leading/trailing whitespace
+ var trimmedSearchString = controller.searchString.replace(/^\s+/, "").replace(/\s+$/, "");
+
+ // Unescape the URI spec for showing as an entry in the popup
+ let url = Components.classes["@mozilla.org/intl/texttosuburi;1"].
+ getService(Components.interfaces.nsITextToSubURI).
+ unEscapeURIForUI("UTF-8", controller.getValueAt(this._currentIndex));
+
+ if (this._currentIndex < existingItemsCount) {
+ // re-use the existing item
+ item = this.richlistbox.childNodes[this._currentIndex];
+
+ // Completely re-use the existing richlistitem if it's the same
+ if (item.getAttribute("text") == trimmedSearchString &&
+ item.getAttribute("url") == url) {
+ item.collapsed = false;
+ this._currentIndex++;
+ continue;
+ }
+ }
+ else {
+ // need to create a new item
+ item = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "richlistitem");
+ }
+
+ var glodaCompleter = Components.
+ classes["@mozilla.org/autocomplete/search;1?name=gloda"].
+ getService(). //Components.interfaces.nsIAutoCompleteSearch)
+ wrappedJSObject;
+ var result = glodaCompleter.curResult;
+
+ // set these attributes before we set the class
+ // so that we can use them from the contructor
+ var row = result.getObjectAt(this._currentIndex);
+ var obj = row.item;
+ item.setAttribute("text", trimmedSearchString);
+ item.setAttribute("type", result.getStyleAt(this._currentIndex));
+
+ item.row = row;
+
+ if (this._currentIndex < existingItemsCount) {
+ // re-use the existing item
+ item._adjustAcItem();
+ item.collapsed = false;
+ }
+ else {
+ // set the class at the end so we can use the attributes
+ // in the xbl constructor
+ item.className = "autocomplete-richlistitem";
+ this.richlistbox.appendChild(item);
+ }
+
+ this._currentIndex++;
+ }
+
+ // yield after each batch of items so that typing the url bar is responsive
+ setTimeout(function (self) { self._appendCurrentResult(); }, 0, this);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <!-- This is autocomplete.xml's autocomplete-richlistitem duplicated and
+ modified to include its useful helper functions, but eliminating anything
+ that assumes specific content sub-items. Namely, url/title/etc. -->
+ <binding id="glodacomplete-base-richlistitem" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+ <implementation implements="nsIDOMXULSelectControlItemElement">
+ <field name="_boundaryCutoff">null</field>
+
+ <property name="boundaryCutoff" readonly="true">
+ <getter>
+ <![CDATA[
+ if (!this._boundaryCutoff) {
+ this._boundaryCutoff =
+ Components.classes["@mozilla.org/preferences-service;1"].
+ getService(Components.interfaces.nsIPrefBranch).
+ getIntPref("toolkit.autocomplete.richBoundaryCutoff");
+ }
+ return this._boundaryCutoff;
+ ]]>
+ </getter>
+ </property>
+
+ <method name="_getBoundaryIndices">
+ <parameter name="aText"/>
+ <parameter name="aSearchTokens"/>
+ <body>
+ <![CDATA[
+ // Short circuit for empty search ([""] == "")
+ if (aSearchTokens == "")
+ return [0, aText.length];
+
+ // Find which regions of text match the search terms
+ let regions = [];
+ for each (let search in aSearchTokens) {
+ let matchIndex;
+ let startIndex = 0;
+ let searchLen = search.length;
+
+ // Find all matches of the search terms, but stop early for perf
+ let lowerText = aText.toLowerCase().substr(0, this.boundaryCutoff);
+ while ((matchIndex = lowerText.indexOf(search, startIndex)) >= 0) {
+ // Start the next search from where this one finished
+ startIndex = matchIndex + searchLen;
+ regions.push([matchIndex, startIndex]);
+ }
+ }
+
+ // Sort the regions by start position then end position
+ regions = regions.sort(function(a, b) let (start = a[0] - b[0])
+ start == 0 ? a[1] - b[1] : start);
+
+ // Generate the boundary indices from each region
+ let start = 0;
+ let end = 0;
+ let boundaries = [];
+ let len = regions.length;
+ for (let i = 0; i < len; i++) {
+ // We have a new boundary if the start of the next is past the end
+ let region = regions[i];
+ if (region[0] > end) {
+ // First index is the beginning of match
+ boundaries.push(start);
+ // Second index is the beginning of non-match
+ boundaries.push(end);
+
+ // Track the new region now that we've stored the previous one
+ start = region[0];
+ }
+
+ // Push back the end index for the current or new region
+ end = Math.max(end, region[1]);
+ }
+
+ // Add the last region
+ boundaries.push(start);
+ boundaries.push(end);
+
+ // Put on the end boundary if necessary
+ if (end < aText.length)
+ boundaries.push(aText.length);
+
+ // Skip the first item because it's always 0
+ return boundaries.slice(1);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_getSearchTokens">
+ <parameter name="aSearch"/>
+ <body>
+ <![CDATA[
+ let search = aSearch.toLowerCase();
+ return search.split(/\s+/);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_needsAlternateEmphasis">
+ <parameter name="aText"/>
+ <body>
+ <![CDATA[
+ for (let i = aText.length; --i >= 0; ) {
+ let charCode = aText.charCodeAt(i);
+ // Arabic, Syriac, Indic languages are likely to have ligatures
+ // that are broken when using the main emphasis styling
+ if (0x0600 <= charCode && charCode <= 0x109F)
+ return true;
+ }
+
+ return false;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_setUpDescription">
+ <parameter name="aDescriptionElement"/>
+ <parameter name="aText"/>
+ <body>
+ <![CDATA[
+ // Get rid of all previous text
+ while (aDescriptionElement.hasChildNodes())
+ aDescriptionElement.removeChild(aDescriptionElement.firstChild);
+
+ // Get the indices that separate match and non-match text
+ let search = this.getAttribute("text");
+ let tokens = this._getSearchTokens(search);
+ let indices = this._getBoundaryIndices(aText, tokens);
+
+ // If we're searching for something that needs alternate emphasis,
+ // we'll need to check the text that we match
+ let checkAlt = this._needsAlternateEmphasis(search);
+
+ let next;
+ let start = 0;
+ let len = indices.length;
+ // Even indexed boundaries are matches, so skip the 0th if it's empty
+ for (let i = indices[0] == 0 ? 1 : 0; i < len; i++) {
+ next = indices[i];
+ let text = aText.substr(start, next - start);
+ start = next;
+
+ if (i % 2 == 0) {
+ // Emphasize the text for even indices
+ let span = aDescriptionElement.appendChild(
+ document.createElementNS("http://www.w3.org/1999/xhtml", "span"));
+ span.className = checkAlt && this._needsAlternateEmphasis(text) ?
+ "ac-emphasize-alt" : "ac-emphasize-text";
+ span.textContent = text;
+ } else {
+ // Otherwise, it's plain text
+ aDescriptionElement.appendChild(document.createTextNode(text));
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_setUpOverflow">
+ <parameter name="aParentBox"/>
+ <parameter name="aEllipsis"/>
+ <body>
+ <![CDATA[
+ // Hide the ellipsis incase there's just enough to not underflow
+ aEllipsis.hidden = true;
+
+ // Start with the parent's width and subtract off its children
+ let tooltip = [];
+ let children = aParentBox.childNodes;
+ let widthDiff = aParentBox.boxObject.width;
+
+ for (let i = 0; i < children.length; i++) {
+ // Only consider a child if it actually takes up space
+ let childWidth = children[i].boxObject.width;
+ if (childWidth > 0) {
+ // Subtract a little less to account for subpixel rounding
+ widthDiff -= childWidth - .5;
+
+ // Add to the tooltip if it's not hidden and has text
+ let childText = children[i].textContent;
+ if (childText)
+ tooltip.push(childText);
+ }
+ }
+
+ // If the children take up more space than the parent.. overflow!
+ if (widthDiff < 0) {
+ // Re-show the ellipsis now that we know it's needed
+ aEllipsis.hidden = false;
+
+ // Separate text components with a ndash --
+ aParentBox.tooltipText = tooltip.join(" \u2013 ");
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_doUnderflow">
+ <parameter name="aName"/>
+ <body>
+ <![CDATA[
+ // Hide the ellipsis right when we know we're underflowing instead of
+ // waiting for the timeout to trigger the _setUpOverflow calculations
+ this[aName + "Box"].tooltipText = "";
+ this[aName + "OverflowEllipsis"].hidden = true;
+ ]]>
+ </body>
+ </method>
+
+ </implementation>
+ </binding>
+
+ <binding id="gloda-single-tag-item" extends="chrome://gloda/content/glodacomplete.xml#glodacomplete-base-richlistitem">
+ <content orient="vertical">
+ <xul:description anonid="explanation"/>
+ </content>
+ <implementation implements="nsIDOMXULSelectControlItemElement">
+ <constructor>
+ <![CDATA[
+ this._explanation = document.getAnonymousElementByAttribute(this, "anonid", "explanation");
+
+ this._adjustAcItem();
+ ]]>
+ </constructor>
+
+ <property name="label" readonly="true">
+ <getter>
+ <![CDATA[
+ return "tag " + this.row.item.tag;
+ ]]>
+ </getter>
+ </property>
+
+ <method name="_adjustAcItem">
+ <body>
+ <![CDATA[
+ this._explanation.value = "messages tagged " + this.row.item.tag;
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="gloda-single-identity-item" extends="chrome://gloda/content/glodacomplete.xml#glodacomplete-base-richlistitem">
+ <content>
+ <xul:hbox>
+ <xul:image anonid="picture"/>
+ <xul:vbox>
+ <xul:hbox>
+ <xul:hbox anonid="name-box" class="ac-title" flex="1"
+ onunderflow="_doUnderflow('_name');">
+ <xul:description anonid="name" class="ac-normal-text ac-comment"
+ xbl:inherits="selected"/>
+ </xul:hbox>
+ <xul:label anonid="name-overflow-ellipsis" xbl:inherits="selected"
+ class="ac-ellipsis-after ac-comment" hidden="true"/>
+ </xul:hbox>
+ <xul:hbox>
+ <xul:hbox anonid="identity-box" class="ac-url" flex="1"
+ onunderflow="_doUnderflow('_identity');">
+ <xul:description anonid="identity" class="ac-normal-text ac-url-text"
+ xbl:inherits="selected"/>
+ </xul:hbox>
+ <xul:label anonid="identity-overflow-ellipsis" xbl:inherits="selected"
+ class="ac-ellipsis-after ac-url-text" hidden="true"/>
+ <xul:image anonid="type-image" class="ac-type-icon"/>
+ </xul:hbox>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ <implementation implements="nsIDOMXULSelectControlItemElement">
+ <constructor>
+ <![CDATA[
+ let ellipsis = "\u2026";
+ try {
+ ellipsis = Components.classes["@mozilla.org/preferences-service;1"].
+ getService(Components.interfaces.nsIPrefBranch).
+ getComplexValue("intl.ellipsis",
+ Components.interfaces.nsIPrefLocalizedString).data;
+ } catch (ex) {
+ // Do nothing.. we already have a default
+ }
+
+ this._identityOverflowEllipsis = document.getAnonymousElementByAttribute(this, "anonid", "identity-overflow-ellipsis");
+ this._nameOverflowEllipsis = document.getAnonymousElementByAttribute(this, "anonid", "name-overflow-ellipsis");
+
+ this._identityOverflowEllipsis.value = ellipsis;
+ this._nameOverflowEllipsis.value = ellipsis;
+
+ this._typeImage = document.getAnonymousElementByAttribute(this, "anonid", "type-image");
+
+ this._identityBox = document.getAnonymousElementByAttribute(this, "anonid", "identity-box");
+ this._identity = document.getAnonymousElementByAttribute(this, "anonid", "identity");
+
+ this._nameBox = document.getAnonymousElementByAttribute(this, "anonid", "name-box");
+ this._name = document.getAnonymousElementByAttribute(this, "anonid", "name");
+
+ this._picture = document.getAnonymousElementByAttribute(this, "anonid", "picture");
+
+ this._adjustAcItem();
+ ]]>
+ </constructor>
+
+ <property name="label" readonly="true">
+ <getter>
+ <![CDATA[
+ var identity = this.row.item;
+ return identity.accessibleLabel;
+ ]]>
+ </getter>
+ </property>
+
+ <method name="_adjustAcItem">
+ <body>
+ <![CDATA[
+ var identity = this.row.item;
+
+ if (identity == null)
+ return;
+
+ // I guess we should get the picture size from CSS or something?
+ this._picture.src = identity.pictureURL(32);
+
+ // Emphasize the matching search terms for the description
+ this._setUpDescription(this._name, identity.contact.name);
+ this._setUpDescription(this._identity, identity.value);
+
+ // Set up overflow on a timeout because the contents of the box
+ // might not have a width yet even though we just changed them
+ setTimeout(this._setUpOverflow, 0, this._nameBox, this._nameOverflowEllipsis);
+ setTimeout(this._setUpOverflow, 0, this._identityBox, this._identityOverflowEllipsis);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="gloda-contact-chunk" extends="chrome://gloda/content/glodacomplete.xml#glodacomplete-base-richlistitem">
+ <content orient="horizontal">
+ <xul:image anonid="picture"/>
+ <xul:vbox>
+ <xul:hbox>
+ <xul:hbox anonid="name-box" class="ac-title" flex="1"
+ onunderflow="_doUnderflow('_name');">
+ <xul:description anonid="name" class="ac-normal-text ac-comment"
+ xbl:inherits="selected"/>
+ </xul:hbox>
+ <xul:label anonid="name-overflow-ellipsis" xbl:inherits="selected"
+ class="ac-ellipsis-after ac-comment" hidden="true"/>
+ </xul:hbox>
+ <xul:hbox>
+ <xul:hbox anonid="identity-box" class="ac-url" flex="1"
+ onunderflow="_doUnderflow('_identity');">
+ <xul:description anonid="identity" class="ac-normal-text ac-url-text"
+ xbl:inherits="selected"/>
+ </xul:hbox>
+ <xul:label anonid="identity-overflow-ellipsis" xbl:inherits="selected"
+ class="ac-ellipsis-after ac-url-text" hidden="true"/>
+ <xul:image anonid="type-image" class="ac-type-icon"/>
+ </xul:hbox>
+ </xul:vbox>
+ </content>
+ <implementation>
+ <constructor>
+ <![CDATA[
+ let ellipsis = "\u2026";
+ try {
+ ellipsis = Components.classes["@mozilla.org/preferences-service;1"].
+ getService(Components.interfaces.nsIPrefBranch).
+ getComplexValue("intl.ellipsis",
+ Components.interfaces.nsIPrefLocalizedString).data;
+ } catch (ex) {
+ // Do nothing.. we already have a default
+ }
+
+ this._identityOverflowEllipsis = document.getAnonymousElementByAttribute(this, "anonid", "identity-overflow-ellipsis");
+ this._nameOverflowEllipsis = document.getAnonymousElementByAttribute(this, "anonid", "name-overflow-ellipsis");
+
+ this._identityOverflowEllipsis.value = ellipsis;
+ this._nameOverflowEllipsis.value = ellipsis;
+
+ this._typeImage = document.getAnonymousElementByAttribute(this, "anonid", "type-image");
+
+ this._identityBox = document.getAnonymousElementByAttribute(this, "anonid", "identity-box");
+ this._identity = document.getAnonymousElementByAttribute(this, "anonid", "identity");
+
+ this._nameBox = document.getAnonymousElementByAttribute(this, "anonid", "name-box");
+ this._name = document.getAnonymousElementByAttribute(this, "anonid", "name");
+
+ this._picture = document.getAnonymousElementByAttribute(this, "anonid", "picture");
+
+ this._adjustAcItem();
+ ]]>
+ </constructor>
+
+ <property name="label" readonly="true">
+ <getter>
+ <![CDATA[
+ var identity = this.obj;
+ return identity.accessibleLabel;
+ ]]>
+ </getter>
+ </property>
+
+ <method name="_adjustAcItem">
+ <body>
+ <![CDATA[
+ var contact = this.obj;
+
+ if (contact == null)
+ return;
+
+ var identity = contact.identities[0];
+
+ // I guess we should get the picture size from CSS or something?
+ this._picture.src = identity.pictureURL(32);
+
+ // Emphasize the matching search terms for the description
+ this._setUpDescription(this._name, contact.name);
+ this._setUpDescription(this._identity, identity.value);
+
+ // Set up overflow on a timeout because the contents of the box
+ // might not have a width yet even though we just changed them
+ setTimeout(this._setUpOverflow, 0, this._nameBox, this._nameOverflowEllipsis);
+ setTimeout(this._setUpOverflow, 0, this._identityBox, this._identityOverflowEllipsis);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="gloda-multi-item" extends="chrome://gloda/content/glodacomplete.xml#glodacomplete-base-richlistitem">
+ <content orient="vertical">
+ <xul:description anonid="explanation"/>
+ <xul:hbox anonid="identity-holder" flex="1">
+ </xul:hbox>
+ </content>
+ <implementation implements="nsIDOMXULSelectControlItemElement">
+ <constructor>
+ <![CDATA[
+ this._explanation = document.getAnonymousElementByAttribute(this, "anonid", "explanation");
+ this._identityHolder = document.getAnonymousElementByAttribute(this, "anonid", "identity-holder");
+
+ this._adjustAcItem();
+ ]]>
+ </constructor>
+
+ <property name="label" readonly="true">
+ <getter>
+ <![CDATA[
+ return this._explanation.value;
+ ]]>
+ </getter>
+ </property>
+
+ <method name="renderItem">
+ <parameter name="aObj"/>
+ <body>
+ var node = document.createElementNS(
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ "richlistitem");
+
+ node.obj = aObj;
+ node.setAttribute("type",
+ "gloda-" + this.row.nounDef.name + "-chunk");
+
+ this._identityHolder.appendChild(node);
+ </body>
+ </method>
+
+ <method name="_adjustAcItem">
+ <body>
+ <![CDATA[
+ // clear out any lingering children
+ while (this._identityHolder.hasChildNodes())
+ this._identityHolder.removeChild(this._identityHolder.firstChild);
+
+ var row = this.row;
+ if (row == null)
+ return;
+
+ this._explanation.value = row.nounDef.name + "s " +
+ row.criteriaType + "ed " + row.criteria;
+
+ // render anyone already in there
+ for each (let item in row.collection.items) {
+ this.renderItem(item);
+ }
+ // listen up, yo.
+ row.renderer = this;
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+
+</bindings>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/content/options.xul
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- ***** 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 Thunderbird Global Database.
+ -
+ - The Initial Developer of the Original Code is
+ - Mozilla Messaging, Inc.
+ - Portions created by the Initial Developer are Copyright (C) 2008
+ - the Initial Developer. All Rights Reserved.
+ -
+ - Contributor(s):
+ - Andrew Sutherland <asutherland@asutherland.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 ***** -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<!DOCTYPE prefwindow SYSTEM "chrome://gloda/locale/prefwindow.dtd">
+<prefwindow id="glodaPreferences" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" title="&prefwindow.title;">
+ <prefpane id="pane1" label="&pane1.title;">
+ <preferences>
+ <preference id="boolpref1" name="extensions.gloda.boolpref" type="bool"/>
+ <preference id="intpref1" name="extensions.gloda.intpref" type="int"/>
+ <preference id="stringpref1" name="extensions.gloda.stringpref" type="string"/> <!-- note that this is only an ASCII string - use unichar for unicode strings -->
+ </preferences>
+ <checkbox id="checkboolpref" preference="boolpref1" label="&checkboolpref.label;" accesskey="&checkboolpref.accesskey;"/>
+ <label accesskey="&intpref.accesskey;" control="textintpref">&intpref.label;</label><textbox id="textintpref" preference="intpref1"/>
+ <label accesskey="&stringpref.accesskey;" control="textstringpref">&stringpref.label;</label><textbox id="textstringpref" preference="stringpref1"/>
+ </prefpane>
+</prefwindow>
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/content/overlay.js
@@ -0,0 +1,68 @@
+/* ***** 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 Thunderbird Global Database.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Messaging, Inc.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Andrew Sutherland <asutherland@asutherland.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 ***** */
+
+// get the core
+Components.utils.import("resource://gloda/modules/public.js");
+
+var gloda = {
+ _mimeMsg: {},
+
+ onLoad: function() {
+ // initialization code
+ this.initialized = true;
+ this.strings = document.getElementById("gloda-strings");
+ },
+ onIndexEverythingCommand: function(e) {
+ GlodaIndexer.indexEverything();
+ },
+ onIndexAddressBookCommand: function(e) {
+ // TODO support address-book indexing or something.
+ },
+ indexSelectedMessages: function () {
+ var dbView = GetDBView();
+ var indices = GetSelectedIndices(dbView);
+ var toindex = [];
+ for (var iIndex=0; iIndex < indices.length; iIndex++) {
+ var actualIndex = indices[iIndex];
+ var folder = dbView.getFolderForViewIndex(actualIndex);
+ var msgKey = dbView.getKeyAt(actualIndex);
+ toindex.push([folder, msgKey]);
+ }
+ GlodaIndexer.indexMessages(toindex);
+ },
+};
+window.addEventListener("load", function(e) { gloda.onLoad(e); }, false);
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/content/thunderbirdOverlay.xul
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- ***** 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 Thunderbird Global Database.
+ -
+ - The Initial Developer of the Original Code is
+ - Mozilla Messaging, Inc.
+ - Portions created by the Initial Developer are Copyright (C) 2008
+ - the Initial Developer. All Rights Reserved.
+ -
+ - Contributor(s):
+ - Andrew Sutherland <asutherland@asutherland.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 ***** -->
+
+<!DOCTYPE overlay SYSTEM "chrome://gloda/locale/gloda.dtd">
+<overlay id="gloda-overlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="overlay.js"/>
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="gloda-strings" src="chrome://gloda/locale/gloda.properties"/>
+ </stringbundleset>
+
+ <menupopup id="taskPopup">
+ <menuitem id="gloda-hello" label="&glodaIndexEverything.label;"
+ oncommand="gloda.onIndexEverythingCommand(event);"/>
+ <menuitem id="gloda-hello" label="&glodaIndexAddressBook.label;"
+ oncommand="gloda.onIndexAddressBookCommand(event);"/>
+ </menupopup>
+
+ <popup id="folderPaneContext">
+ <menuseparator id="folderPaneContext-sep-gloda"/>
+ <menuitem id="folderPaneContext-gloda-index"
+ label="&folderContextGlodaIndex.label;"
+ accesskey="&folderContextGlodaIndex.accesskey;"
+ oncommand="GlodaIndexer.indexFolderByURI(GetSelectedFolderURI());"/>
+ </popup>
+
+ <popup id="threadPaneContext">
+ <menuseparator id="threadPaneContext-sep-gloda"/>
+ <menuitem id="threadPaneContext-gloda-index"
+ label="&threadContextGlodaIndex.label;"
+ accesskey="&threadContextGlodaIndex.accesskey;"
+ oncommand="gloda.indexSelectedMessages();"/>
+ </popup>
+
+</overlay>
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/defaults/preferences/gloda.js
@@ -0,0 +1,5 @@
+pref("extensions.gloda.boolpref", false);
+pref("extensions.gloda.intpref", 0);
+pref("extensions.gloda.stringpref", "A string");
+// See http://kb.mozillazine.org/Localize_extension_descriptions
+pref("extensions.gloda@mozillamessaging.com.description", "chrome://gloda/locale/gloda.properties");
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/install.rdf
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>gloda@mozillamessaging.com</em:id>
+ <em:name>Global Database</em:name>
+ <em:version>0.0.1</em:version>
+ <em:creator>Mozilla Messaging, Inc.</em:creator>
+ <em:contributor>Andrew Sutherland <asutherland@asutherland.org></em:contributor>
+ <em:description>Adds a global database to Thunderbird</em:description>
+ <em:optionsURL>chrome://gloda/content/options.xul</em:optionsURL>
+ <em:targetApplication>
+ <Description>
+ <em:id>{3550f703-e582-4d05-9a08-453d09bdfdc6}</em:id> <!-- thunderbird -->
+ <em:minVersion>3.0a2pre</em:minVersion>
+ <em:maxVersion>3.0.*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/jar.mn
@@ -0,0 +1,11 @@
+gloda.jar:
+% content gloda %content/
+% locale gloda en-US %locale/en-US/
+% overlay chrome://messenger/content/messenger.xul chrome://gloda/content/thunderbirdOverlay.xul
+% resource gloda ../
+ locale/en-US/gloda.dtd (locale/en-US/gloda.dtd)
+ locale/en-US/gloda.properties (locale/en-US/gloda.properties)
+ content/overlay.js (content/overlay.js)
+ content/thunderbirdOverlay.xul (content/thunderbirdOverlay.xul)
+ content/glodacomplete.css (content/glodacomplete.css)
+ content/glodacomplete.xml (content/glodacomplete.xml)
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/locale/en-US/gloda.dtd
@@ -0,0 +1,6 @@
+<!ENTITY glodaIndexEverything.label "Index Everything">
+<!ENTITY glodaIndexAddressBook.label "Index Addressbook">
+<!ENTITY folderContextGlodaIndex.label "Gloda Index">
+<!ENTITY folderContextGlodaIndex.accesskey "x">
+<!ENTITY threadContextGlodaIndex.label "Gloda Index">
+<!ENTITY threadContextGlodaIndex.accesskey "x">
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/locale/en-US/gloda.properties
@@ -0,0 +1,8 @@
+prefMessage=Int Pref Value: %d
+extensions.gloda.description=Adds a global database to Thunderbird
+shutdownTaskName=Global Database Indexer
+actionIdle=Idle
+actionIndexing=Indexing
+actionDeindexing=De-Indexing
+actionMoving=Move Processing
+messageIndexingExplanation=some messages
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/locale/en-US/prefwindow.dtd
@@ -0,0 +1,8 @@
+<!ENTITY prefwindow.title "Global Database Has No Preferences">
+<!ENTITY pane1.title "Global Database Has No Preferences">
+<!ENTITY checkboolpref.label "I like cookies">
+<!ENTITY checkboolpref.accesskey "l">
+<!ENTITY intpref.label "I would eat this many cookies">
+<!ENTITY intpref.accesskey "e">
+<!ENTITY stringpref.label "Name of my favorite cookie">
+<!ENTITY stringpref.accesskey "f">
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/Makefile.in
@@ -0,0 +1,50 @@
+#
+# ***** 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 mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Mozilla Messaging, Inc.
+#
+# Portions created by the Initial Developer are Copyright (C) 2008
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of 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 *****
+
+DEPTH = ../../../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MODULE = mailnewsglobaldb
+
+EXTRA_JS_MODULES = $(wildcard $(srcdir)/*.js)
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/collection.js
@@ -0,0 +1,718 @@
+/* ***** 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 Thunderbird Global Database.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Messaging, Inc.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Andrew Sutherland <asutherland@asutherland.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 ***** */
+
+EXPORTED_SYMBOLS = ['GlodaCollection', 'GlodaCollectionManager'];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gloda/modules/log4moz.js");
+
+const LOG = Log4Moz.Service.getLogger("gloda.collection");
+
+/**
+ * @namespace Central registry and logic for all collections.
+ *
+ * The collection manager is a singleton that has the following tasks:
+ * - Let views of objects (nouns) know when their objects have changed. For
+ * example, an attribute has changed due to user action.
+ * - Let views of objects based on queries know when new objects match their
+ * query, or when their existing objects no longer match due to changes.
+ * - Caching/object-identity maintenance. It is ideal if we only ever have
+ * one instance of an object at a time. (More specifically, only one instance
+ * per database row 'id'.) The collection mechanism lets us find existing
+ * instances to this end. Caching can be directly integrated by being treated
+ * as a special collection.
+ */
+var GlodaCollectionManager = {
+ _collectionsByNoun: {},
+ _cachesByNoun: {},
+
+ /**
+ * Registers the existence of a collection with the collection manager. This
+ * is done using a weak reference so that the collection can go away if it
+ * wants to.
+ */
+ registerCollection: function gloda_colm_registerCollection(aCollection) {
+ let collections;
+ let nounID = aCollection.query._nounDef.id;
+ if (!(nounID in this._collectionsByNoun))
+ collections = this._collectionsByNoun[nounID] = [];
+ else {
+ // purge dead weak references while we're at it
+ collections = this._collectionsByNoun[nounID].filter(function (aRef) {
+ return aRef.get(); });
+ this._collectionsByNoun[nounID] = collections;
+ }
+ collections.push(Cu.getWeakReference(aCollection));
+ },
+
+ getCollectionsForNounID: function gloda_colm_getCollectionsForNounID(aNounID){
+ if (!(aNounID in this._collectionsByNoun))
+ return [];
+
+ // generator would be nice, but I suspect get() is too expensive to use
+ // twice (guard/predicate and value)
+ let weakCollections = this._collectionsByNoun[aNounID];
+ let collections = [];
+ for (let iColl = 0; iColl < weakCollections.length; iColl++) {
+ let collection = weakCollections[iColl].get();
+ if (collection)
+ collections.push(collection);
+ }
+ return collections;
+ },
+
+ defineCache: function gloda_colm_defineCache(aNounDef, aCacheSize) {
+ this._cachesByNoun[aNounDef.id] = new GlodaLRUCacheCollection(aNounDef,
+ aCacheSize);
+ },
+
+ /**
+ * Attempt to locate an instance of the object of the given noun type with the
+ * given id. Counts as a cache hit if found. (And if it was't in a cache,
+ * but rather a collection, it is added to the cache.)
+ */
+ cacheLookupOne: function gloda_colm_cacheLookupOne(aNounID, aID, aDoCache) {
+ let cache = this._cachesByNoun[aNounID];
+
+ if (cache) {
+ if (aID in cache._idMap) {
+ let item = cache._idMap[aID];
+ return cache.hit(item);
+ }
+ }
+
+ if (aDoCache === false)
+ cache = null;
+
+ for each (let [iCollection, collection] in
+ Iterator(this.getCollectionsForNounID(aNounID))) {
+ if (aID in collection._idMap) {
+ let item = collection._idMap[aID];
+ if (cache)
+ cache.add([item]);
+ return item;
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Lookup multiple nouns by ID from the cache/existing collections.
+ * @return [The number that were found, the number that were not found.]
+ */
+ cacheLookupMany: function gloda_colm_cacheLookupMany(aNounID, aIDMap,
+ aTargetMap, aDoCache) {
+ let foundCount = 0, notFoundCount = 0, notFound = {};
+
+ let cache = this._cachesByNoun[aNounID];
+
+ if (cache) {
+ for (let key in aIDMap) {
+ let cacheValue = cache._idMap[key];
+ if (cacheValue === undefined) {
+ notFoundCount++;
+ notFound[key] = null;
+ }
+ else {
+ foundCount++;
+ aTargetMap[key] = cacheValue;
+ cache.hit(cacheValue);
+ }
+ }
+ }
+
+ if (aDoCache === false)
+ cache = null;
+
+ for each (let [iCollection, collection] in
+ Iterator(this.getCollectionsForNounID(aNounID))) {
+ for (let key in notFound) {
+ let collValue = collection._idMap[key];
+ if (collValue !== undefined) {
+ aTargetMap[key] = collValue;
+ delete notFound[key];
+ foundCount++;
+ notFoundCount--;
+ if (cache)
+ cache.add([collValue]);
+ }
+ }
+ }
+
+ return [foundCount, notFoundCount, notFound];
+ },
+
+ /**
+ * Attempt to locate an instance of the object of the given noun type with the
+ * given id. Counts as a cache hit if found. (And if it was't in a cache,
+ * but rather a collection, it is added to the cache.)
+ */
+ cacheLookupOneByUniqueValue:
+ function gloda_colm_cacheLookupOneByUniqueValue(aNounID, aUniqueValue,
+ aDoCache) {
+ let cache = this._cachesByNoun[aNounID];
+
+ if (cache) {
+ if (aUniqueValue in cache._uniqueValueMap) {
+ let item = cache._uniqueValueMap[aUniqueValue];
+ return cache.hit(item);
+ }
+ }
+
+ if (aDoCache === false)
+ cache = null;
+
+ for each (let [iCollection, collection] in
+ Iterator(this.getCollectionsForNounID(aNounID))) {
+ if (aUniqueValue in collection._uniqueValueMap) {
+ let item = collection._uniqueValueMap[aUniqueValue];
+ if (cache)
+ cache.add([item]);
+ return item;
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Checks whether the provided item with the given id is actually a duplicate
+ * of an instance that already exists in the cache/a collection. If it is,
+ * the pre-existing instance is returned and counts as a cache hit. If it
+ * is not, the passed-in instance is added to the cache and returned.
+ */
+ cacheLoadUnifyOne: function gloda_colm_cacheLoadUnifyOne(aItem) {
+ let items = [aItem];
+ this.cacheLoadUnify(aItem.NOUN_ID, items);
+ return items[0];
+ },
+
+ /**
+ * Given a list of items, check if any of them already have duplicate,
+ * canonical, instances in the cache or collections. Items with pre-existing
+ * instances are replaced by those instances in the provided list, and each
+ * counts as a cache hit. Items without pre-existing instances are added
+ * to the cache and left intact.
+ */
+ cacheLoadUnify: function gloda_colm_cacheLoadUnify(aNounID, aItems,
+ aCacheIfMissing) {
+ let cache = this._cachesByNoun[aNounID];
+ if (aCacheIfMissing === undefined)
+ aCacheIfMissing = true;
+
+ // track the items we haven't yet found in a cache/collection (value) and
+ // their index in aItems (key). We're somewhat abusing the dictionary
+ // metaphor with the intent of storing tuples here. We also do it because
+ // it allows random-access deletion theoretically without cost. (Since
+ // we delete during iteration, that may be wrong, but it sounds like the
+ // semantics still work?)
+ let unresolvedIndexToItem = {};
+ let numUnresolved = 0;
+
+ if (cache) {
+ for (let iItem = 0; iItem < aItems.length; iItem++) {
+ let item = aItems[iItem];
+
+ if (item.id in cache._idMap) {
+ let realItem = cache._idMap[item.id];
+ // update the caller's array with the reference to the 'real' item
+ aItems[iItem] = realItem;
+ cache.hit(realItem);
+ }
+ else {
+ unresolvedIndexToItem[iItem] = item;
+ numUnresolved++;
+ }
+ }
+
+ // we're done if everyone was a hit.
+ if (numUnresolved == 0)
+ return;
+ }
+ else {
+ for (let iItem = 0; iItem < aItems.length; iItem++) {
+ unresolvedIndexToItem[iItem] = aItems[iItem];
+ }
+ numUnresolved = aItems.length;
+ }
+
+ let needToCache = [];
+ // next, let's fall back to our collections
+ for each (let [iCollection, collection] in
+ Iterator(this.getCollectionsForNounID(aNounID))) {
+ for (let [iItem, item] in Iterator(unresolvedIndexToItem)) {
+ if (item.id in collection._idMap) {
+ let realItem = collection._idMap[item.id];
+ // update the caller's array to now have the 'real' object
+ aItems[iItem] = realItem;
+ // flag that we need to cache this guy (we use an inclusive cache)
+ needToCache.push(realItem);
+ // we no longer need to resolve this item...
+ delete unresolvedIndexToItem[iItem];
+ // stop checking collections if we got everybody
+ if (--numUnresolved == 0)
+ break;
+ }
+ }
+ }
+
+ // anything left in unresolvedIndexToItem should be added to the cache
+ // unless !aCacheIfMissing. plus, we already have 'needToCache'
+ if (cache && aCacheIfMissing) {
+ cache.add(needToCache.concat([val for each
+ (val in unresolvedIndexToItem)]));
+ }
+
+ return aItems;
+ },
+
+ cacheCommitDirty: function glod_colm_cacheCommitDirty() {
+ for each (let cache in this._cachesByNoun) {
+ cache.commitDirty();
+ }
+ },
+
+ /**
+ * Notifies the collection manager that an item has been loaded and should
+ * be cached, assuming caching is active.
+ */
+ itemLoaded: function gloda_colm_itemsLoaded(aItem) {
+ let cache = this._cachesByNoun[aItem.NOUN_ID];
+ if (cache) {
+ cache.add([aItem]);
+ }
+ },
+
+ /**
+ * Notifies the collection manager that multiple items has been loaded and
+ * should be cached, assuming caching is active.
+ */
+ itemsLoaded: function gloda_colm_itemsLoaded(aNounID, aItems) {
+ let cache = this._cachesByNoun[aNounID];
+ if (cache) {
+ cache.add(aItems);
+ }
+ },
+
+ /**
+ * This should be called when items are added to the global database. This
+ * should generally mean during indexing by indexers or an attribute
+ * provider.
+ * We walk all existing collections for the given noun type and add the items
+ * to the collection if the item meets the query that defines the collection.
+ */
+ itemsAdded: function gloda_colm_itemsAdded(aNounID, aItems) {
+ let cache = this._cachesByNoun[aNounID];
+ if (cache) {
+ cache.add(aItems);
+ }
+
+ for each (let [iCollection, collection] in
+ Iterator(this.getCollectionsForNounID(aNounID))) {
+ let addItems = [item for each ([i, item] in Iterator(aItems))
+ if (collection.query.test(item))];
+ if (addItems.length)
+ collection._onItemsAdded(addItems);
+ }
+ },
+ /**
+ * This should be called when items in the global database are modified. For
+ * example, as a result of indexing. This should generally only be called
+ * by indexers or by attribute providers.
+ * We walk all existing collections for the given noun type. For items
+ * currently included in each collection but should no longer be (per the
+ * collection's defining query) we generate onItemsRemoved events. For items
+ * not currently included in the collection but should now be, we generate
+ * onItemsAdded events. For items included that still match the query, we
+ * generate onItemsModified events.
+ */
+ itemsModified: function gloda_colm_itemsModified(aNounID, aItems) {
+ for each (let [iCollection, collection] in
+ Iterator(this.getCollectionsForNounID(aNounID))) {
+ let added = [], modified = [], removed = [];
+ for each (let [iItem, item] in Iterator(aItems)) {
+ if (item.id in collection._idMap) {
+ // currently in... but should it still be there?
+ if (collection.query.test(item))
+ modified.push(item); // yes, keep it
+ else
+ removed.push(item); // no, bin it
+ }
+ else if (collection.query.test(item)) // not in, should it be?
+ added.push(item); // yep, add it
+ }
+ if (added.length)
+ collection._onItemsAdded(added);
+ if (modified.length)
+ collection._onItemsModified(modified);
+ if (removed.length)
+ collection._onItemsRemoved(removed);
+ }
+ },
+ /**
+ * This should be called when items in the global database are permanently
+ * deleted. (This is distinct from concepts like message deletion which may
+ * involved trash folders or other modified forms of existence. Deleted
+ * means the data is gone and if it were to come back, it would come back
+ * with a brand new unique id and we would get an itemsAdded event.)
+ * We walk all existing collections for the given noun type. For items
+ * currently in the collection, we generate onItemsRemoved events.
+ */
+ itemsDeleted: function gloda_colm_itemsDeleted(aNounID, aItems) {
+ // cache
+ let cache = this._cachesByNoun[aNounID];
+ if (cache) {
+ for each (let [iItem, item] in Iterator(aItem)) {
+ if (item.id in cache._idMap)
+ cache.delete(item);
+ }
+ }
+
+ // collections
+ for each (let [iCollection, collection] in
+ Iterator(this.getCollectionsForNounID(aNounID))) {
+ let removeItems = [item for each ([i, item] in Iterator(aItems))
+ if (item.id in collection._idMap)];
+ if (removeItems.length)
+ collection._onItemsRemoved(removeItems);
+ }
+ },
+};
+
+/**
+ * @class A current view of the set of first-class nouns meeting a given query.
+ * Assuming a listener is present, events are
+ * generated when new objects meet the query, existing objects no longer meet
+ * the query, or existing objects have experienced a change in attributes that
+ * does not affect their ability to be present (but the listener may care about
+ * because it is exposing those attributes).
+ * @constructor
+ */
+function GlodaCollection(aNounDef, aItems, aQuery, aListener,
+ aMasterCollection) {
+ // if aNounDef is null, we are just being invoked for subclassing
+ if (aNounDef === undefined)
+ return;
+
+ this._nounDef = aNounDef;
+ // should we also maintain a unique value mapping...
+ if (this._nounDef.usesUniqueValue)
+ this._uniqueValueMap = {};
+
+ this.pendingItems = [];
+ this._pendingIdMap = {};
+ this.items = [];
+ this._idMap = {};
+
+ // force the listener to null for our call to _onItemsAdded; no events for
+ // the initial load-out.
+ this._listener = null;
+ if (aItems && aItems.length)
+ this._onItemsAdded(aItems);
+
+ this.query = aQuery || null;
+ if (this.query)
+ this.query.collection = this;
+ this._listener = aListener || null;
+
+ this.deferredCount = 0;
+ this.resolvedCount = 0;
+
+ if (aMasterCollection) {
+ this.masterCollection = aMasterCollection.masterCollection;
+ }
+ else {
+ this.masterCollection = this;
+ /** a dictionary of dictionaries. at the top level, the keys are noun IDs.
+ * each of these sub-dictionaries maps the IDs of desired noun instances to
+ * the actual instance, or null if it has not yet been loaded.
+ */
+ this.referencesByNounID = {};
+ /**
+ * a dictionary of dictionaries. at the top level, the keys are noun IDs.
+ * each of the sub-dictionaries maps the IDs of the _recognized parent
+ * noun_ to the list of children, or null if the list has not yet been
+ * populated.
+ *
+ * So if we have a noun definition A with ID 1 who is the recognized parent
+ * noun of noun definition B with ID 2, AND we have an instance A(1) with
+ * two children B(10), B(11), then an example might be: {2: {1: [10, 11]}}.
+ */
+ this.inverseReferencesByNounID = {};
+ this.subCollections = {};
+ }
+}
+
+GlodaCollection.prototype = {
+ get listener() { return this._listener; },
+ set listener(aListener) { this._listener = aListener; },
+
+ /**
+ * Clear the contents of this collection. This only makes sense for explicit
+ * collections or wildcard collections. (Actual query-based collections
+ * should represent the state of the query, so unless we're going to delete
+ * all the items, clearing the collection would violate that constraint.)
+ */
+ clear: function gloda_coll_clear() {
+ this._idMap = {};
+ if (this._uniqueValueMap)
+ this._uniqueValueMap = {};
+ this.items = [];
+ },
+
+ _onItemsAdded: function gloda_coll_onItemsAdded(aItems) {
+ this.items.push.apply(this.items, aItems);
+ if (this._uniqueValueMap) {
+ for each (let [iItem, item] in Iterator(this.items)) {
+ this._idMap[item.id] = item;
+ this._uniqueValueMap[item.uniqueValue] = item;
+ }
+ }
+ else {
+ for each (let [iItem, item] in Iterator(this.items)) {
+ this._idMap[item.id] = item;
+ }
+ }
+ if (this._listener) {
+ try {
+ this._listener.onItemsAdded(aItems, this);
+ }
+ catch (ex) {
+ LOG.error("caught exception from listener in onItemsAdded: " +
+ ex.fileName + ":" + ex.lineNumber + ": " + ex);
+ }
+ }
+ },
+
+ _onItemsModified: function gloda_coll_onItemsModified(aItems) {
+ if (this._listener) {
+ try {
+ this._listener.onItemsModified(aItems, this);
+ }
+ catch (ex) {
+ LOG.error("caught exception from listener in onItemsModified: " +
+ ex.fileName + ":" + ex.lineNumber + ": " + ex);
+ }
+ }
+ },
+
+ /**
+ * Given a list of items that definitely no longer belong in this collection,
+ * remove them from the collection and notify the listener. The 'tricky'
+ * part is that we need to remove the deleted items from our list of items.
+ */
+ _onItemsRemoved: function gloda_coll_onItemsRemoved(aItems) {
+ // we want to avoid the O(n^2) deletion performance case, and deletion
+ // should be rare enough that the extra cost of building the deletion map
+ // should never be a real problem.
+ let deleteMap = {};
+ // build the delete map while also nuking from our id map/unique value map
+ for each (let [iItem, item] in Iterator(aItems)) {
+ deleteMap[item.id] = true;
+ delete this._idMap[item.id];
+ if (this._uniqueValueMap)
+ delete this._uniqueValueMap[item.uniqueValue];
+ }
+ let items = this.items;
+ // in-place filter. probably needless optimization.
+ let iWrite=0;
+ for (let iRead = 0; iRead < items.length; iRead++) {
+ let item = items[iRead];
+ if (!(item.id in deleteMap))
+ items[iWrite++] = item;
+ }
+ items.slice(iWrite);
+
+ if (this._listener) {
+ try {
+ this._listener.onItemsRemoved(aItems, this);
+ }
+ catch (ex) {
+ LOG.error("caught exception from listener in onItemsRemoved: " +
+ ex.fileName + ":" + ex.lineNumber + ": " + ex);
+ }
+ }
+ },
+
+ _onQueryCompleted: function gloda_coll_onQueryCompleted() {
+ if (this._listener && this._listener.onQueryCompleted)
+ this._listener.onQueryCompleted(this);
+ }
+};
+
+/**
+ * Create an LRU cache collection for the given noun with the given size.
+ * @constructor
+ */
+function GlodaLRUCacheCollection(aNounDef, aCacheSize) {
+ GlodaCollection.call(this, aNounDef, null, null, null);
+
+ this._head = null; // aka oldest!
+ this._tail = null; // aka newest!
+ this._size = 0;
+ // let's keep things sane, and simplify our logic a little...
+ if (aCacheSize < 32)
+ aCacheSize = 32;
+ this._maxCacheSize = aCacheSize;
+}
+/**
+ * @class A LRU-discard cache. We use a doubly linked-list for the eviction
+ * tracking. Since we require that there is at most one LRU-discard cache per
+ * noun class, we simplify our lives by adding our own attributes to the
+ * cached objects.
+ * @augments GlodaCollection
+ */
+GlodaLRUCacheCollection.prototype = new GlodaCollection;
+GlodaLRUCacheCollection.prototype.add = function cache_add(aItems) {
+ for each (let [iItem, item] in Iterator(aItems)) {
+ if (item.id in this._idMap) {
+ // DEBUGME so, we're dealing with this, but it shouldn't happen. need
+ // trace-debuggage.
+ continue;
+ }
+ this._idMap[item.id] = item;
+ if (this._uniqueValueMap)
+ this._uniqueValueMap[item.uniqueValue] = item;
+
+ item._lruPrev = this._tail;
+ // we do have to make sure that we will set _head the first time we insert
+ // something
+ if (this._tail !== null)
+ this._tail._lruNext = item;
+ else
+ this._head = item;
+ item._lruNext = null;
+ this._tail = item;
+
+ this._size++;
+ }
+
+ while (this._size > this._maxCacheSize) {
+ let item = this._head;
+
+ // we never have to deal with the possibility of needing to make _head/_tail
+ // null.
+ this._head = item._lruNext;
+ this._head._lruPrev = null;
+ // (because we are nice, we will delete the properties...)
+ delete item._lruNext;
+ delete item._lruPrev;
+
+ // nuke from our id map
+ delete this._idMap[item.id];
+ if (this._uniqueValueMap)
+ delete this._uniqueValueMap[item.uniqueValue];
+
+ // flush dirty items to disk (they may not have this attribute, in which
+ // case, this returns false, which is fine.)
+ if (item.dirty) {
+ this._nounDef.objUpdate.call(this._nounDef.datastore, item);
+ delete item.dirty;
+ }
+
+ this._size--;
+ }
+};
+
+GlodaLRUCacheCollection.prototype.hit = function cache_hit(aItem) {
+ // don't do anything in the 0 or 1 items case, or if we're already
+ // the last item
+ if ((this._head === this._tail) || (this._tail === aItem))
+ return aItem;
+
+ // - unlink the item
+ if (aItem._lruPrev !== null)
+ aItem._lruPrev._lruNext = aItem._lruNext;
+ else
+ this._head = aItem._lruNext;
+ // (_lruNext cannot be null)
+ aItem._lruNext._lruPrev = aItem._lruPrev;
+ // - link it in to the end
+ this._tail._lruNext = aItem;
+ aItem._lruPrev = this._tail;
+ aItem._lruNext = null;
+ // update tail tracking
+ this._tail = aItem;
+
+ return aItem;
+};
+
+GlodaLRUCacheCollection.prototype.deleted = function cache_deleted(aItem) {
+ // unlink the item
+ if (aItem._lruPrev !== null)
+ aItem._lruPrev._lruNext = aItem._lruNext;
+ else
+ this._head = aItem._lruNext;
+ if (aItem._lruNext !== null)
+ aItem._lruNext._lruPrev = aItem._lruPrev;
+ else
+ this._tail = aItem._lruPrev;
+
+ // (because we are nice, we will delete the properties...)
+ delete aItem._lruNext;
+ delete aItem._lruPrev;
+
+ // nuke from our id map
+ delete this._idMap[aItem.id];
+ if (this._uniqueValueMap)
+ delete this._uniqueValueMap[aItem.uniqueValue];
+
+ this._size--;
+}
+
+/**
+ * If any of the cached items are dirty, commit them, and make them no longer
+ * dirty.
+ */
+GlodaLRUCacheCollection.prototype.commitDirty = function cache_commitDirty() {
+ // we can only do this if there is an update method available...
+ if (!this._nounDef.objUpdate)
+ return;
+
+ for each (let [iItem, item] in Iterator(this._idMap)) {
+ if (item.dirty) {
+ LOG.debug("flushing dirty: " + item);
+ this._nounDef.objUpdate.call(this._nounDef.datastore, item);
+ delete item.dirty;
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/connotent.js
@@ -0,0 +1,229 @@
+/* ***** 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 Thunderbird Global Database.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Messaging, Inc.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Andrew Sutherland <asutherland@asutherland.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 ***** */
+
+EXPORTED_SYMBOLS = ['GlodaContent'];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gloda/modules/log4moz.js");
+
+const LOG = Log4Moz.Service.getLogger("gloda.connotent");
+
+function GlodaContent() {
+ this._contentPriority = null;
+ this._producing = false;
+ this._hunks = [];
+}
+
+GlodaContent.prototype = {
+ kPriorityBase: 0,
+ kPriorityPerfect: 100,
+
+ kHunkMeta: 1,
+ kHunkQuoted: 2,
+ kHunkContent: 3,
+
+ _resetContent: function gloda_content__resetContent() {
+ this._keysAndValues = [];
+ this._keysAndDeltaValues = [];
+ this._hunks = [];
+ this._curHunk = null;
+ },
+
+ /* ===== Consumer API ===== */
+ hasContent: function gloda_content_hasContent() {
+ return (this._contentPriority != null);
+ },
+
+ /**
+ * Return content suitable for snippet display. This means that no quoting
+ * or meta-data should be returned.
+ *
+ * @param aMaxLength The maximum snippet length desired.
+ */
+ getContentSnippet: function gloda_content_getContentSnippet(aMaxLength) {
+ let content = this.getContentString();
+ if (aMaxLength)
+ content = content.substring(0, aMaxLength);
+ return content;
+ },
+
+ getContentString: function gloda_content_getContent(aIndexingPurposes) {
+ let data = "";
+ for each (let [, hunk] in Iterator(this._hunks)) {
+ if (hunk.hunkType == this.kHunkContent) {
+ if (data)
+ data += "\n" + hunk.data;
+ else
+ data = hunk.data;
+ }
+ }
+
+ if (aIndexingPurposes) {
+ // append the values for indexing. we assume the keywords are cruft.
+ // this may be crazy, but things that aren't a science aren't an exact
+ // science.
+ for each (let [, kv] in Iterator(this._keysAndValues)) {
+ data += "\n" + kv[1];
+ }
+ for each (let [, kon] in Iterator(this._keysAndValues)) {
+ data += "\n" + kon[1] + "\n" + kon[2];
+ }
+ }
+
+ return data;
+ },
+
+ /* ===== Producer API ===== */
+ /**
+ * Called by a producer with the priority they believe their interpretation
+ * of the content comes in at.
+ *
+ * @returns true if we believe the producer's interpretation will be
+ * interesting and they should go ahead and generate events. We return
+ * false if we don't think they are interesting, in which case they should
+ * probably not issue calls to us, although we don't care. (We will
+ * ignore their calls if we return false, this allows the simplification
+ * of code that needs to run anyways.)
+ */
+ volunteerContent: function gloda_content_volunteerContent(aPriority) {
+ if (this._contentPriority === null || this._contentPriority < aPriority) {
+ this._contentPriority = aPriority;
+ this._resetContent();
+ this._producing = true;
+ return true;
+ }
+ this._producing = false;
+ return false;
+ },
+
+ keyValue: function gloda_content_keyValue(aKey, aValue) {
+ if (!this._producing)
+ return;
+
+ this._keysAndValues.push([aKey, aValue]);
+ },
+ keyValueDelta: function gloda_content_keyValueDelta (aKey, aOldValue,
+ aNewValue) {
+ if (!this._producing)
+ return;
+
+ this._keysAndDeltaValues.push([aKey, aOldValue, aNewValue]);
+ },
+
+ /**
+ * Meta lines are lines that have to do with the content but are not the
+ * content and can generally be related to an attribute that has been derived
+ * and stored on the item.
+ * For example, a bugzilla bug may note that an attachment was created; this
+ * is not content and wouldn't be desired in a snippet, but is still
+ * potentially interesting meta-data.
+ *
+ * @param aLineOrLines The line or list of lines that are meta-data.
+ * @param aAttr The attribute this meta-data is associated with.
+ * @param aIndex If the attribute is non-singular, indicate the specific
+ * index of the item in the attribute's bound list that the meta-data
+ * is associated with.
+ */
+ meta: function gloda_content_meta(aLineOrLines, aAttr, aIndex) {
+ if (!this._producing)
+ return;
+
+ let data;
+ if (typeof(aLineOrLines) == "string")
+ data = aLineOrLines;
+ else
+ data = aLineOrLines.join("\n");
+
+ this._curHunk = {hunkType: this.kHunkMeta, attr: aAttr, index: aIndex,
+ data: data};
+ this._hunks.push(this._curHunk);
+ },
+ /**
+ * Quoted lines reference previous messages or what not.
+ *
+ * @param aLineOrLiens The line or list of lines that are quoted.
+ * @param aDepth The depth of the quoting.
+ * @param aOrigin The item that originated the original content, if known.
+ * For example, perhaps a GlodaMessage?
+ * @param aTarget A reference to the location in the original content, if
+ * known. For example, the index of a line in a message or something?
+ */
+ quoted: function gloda_content_quoted(aLineOrLines, aDepth, aOrigin,
+ aTarget) {
+ if (!this._producing)
+ return;
+
+ let data;
+ if (typeof(aLineOrLines) == "string")
+ data = aLineOrLines;
+ else
+ data = aLineOrLines.join("\n");
+
+ if (!this._curHunk ||
+ this._curHunk.hunkType != this.kHunkQuoted ||
+ this._curHunk.depth != aDepth ||
+ this._curHunk.origin != aOrigin || this._curHunk.target != aTarget) {
+ this._curHunk = {hunkType: this.kHunkQuoted, data: data,
+ depth: aDepth, origin: aOrigin, target: aTarget};
+ this._hunks.push(this._curHunk);
+ }
+ else
+ this._curHunk.data += "\n" + data;
+ },
+
+ content: function gloda_content_content(aLineOrLines) {
+ if (!this._producing)
+ return;
+
+ let data;
+ if (typeof(aLineOrLines) == "string")
+ data = aLineOrLines;
+ else
+ data = aLineOrLines.join("\n");
+
+ if (!this._curHunk || this._curHunk.hunkType != this.kHunkContent) {
+ this._curHunk = {hunkType: this.kHunkContent, data: data};
+ this._hunks.push(this._curHunk);
+ }
+ else
+ this._curHunk.data += "\n" + data;
+ },
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/databind.js
@@ -0,0 +1,147 @@
+/* ***** 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 Thunderbird Global Database.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Messaging, Inc.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Andrew Sutherland <asutherland@asutherland.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 ***** */
+
+EXPORTED_SYMBOLS = ["GlodaDatabind"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gloda/modules/log4moz.js");
+
+function DatabindCallback(aDatabind, aCallbackThis, aCallback, aOneShot) {
+ this._databind = aDatabind;
+ this._callbackThis = aCallbackThis;
+ this._callback = aCallback;
+ this._oneShot = aOneShot;
+ this._databind._datastore._pendingAsyncStatements++;
+}
+DatabindCallback.prototype = {
+ handleResult: function (aResultSet) {
+ let rows = [];
+ let rowResult;
+ let getVariant = this._databind._datastore._getVariant;
+ while (rowResult = aResultSet.getNextRow()) {
+ let row = {};
+ for each (let [iCol, colDef] in
+ Iterator(this._databind._tableDef.columns)) {
+ let colName = colDef[0];
+ row[colName] = getVariant(rowResult, iCol);
+ }
+ rows.push(row);
+ }
+ this._callback.call(this._callbackThis, rows, false);
+ },
+ handleError: function (aError) {
+ },
+ handleCompletion: function () {
+ this._callback.call(this._callbackThis, [], true);
+ this._databind._datastore._asyncCompleted();
+ },
+}
+
+function GlodaDatabind(aTableDef, aDatastore) {
+ this._tableDef = aTableDef;
+ this._datastore = aDatastore;
+ this._log = Log4Moz.Service.getLogger("gloda.databind." + aTableDef.name);
+
+ let insertSql = "INSERT INTO " + this._tableDef._realName + " (" +
+ [coldef[0] for each
+ ([i, coldef] in Iterator(this._tableDef.columns))].join(", ") +
+ ") VALUES (" +
+ [(":" + coldef[0]) for each
+ ([i, coldef] in Iterator(this._tableDef.columns))].join(", ") +
+ ")";
+
+ this._insertStmt = aDatastore._createAsyncStatement(insertSql);
+
+ this._stmtCache = {};
+}
+
+GlodaDatabind.prototype = {
+ /*
+ getHighId: function(aLessThan) {
+ let sql = "select MAX(id) AS m_id FROM " + this._tableDef._realName;
+ if (aLessThan !== undefined)
+ sql += " WHERE id < " + aLessThan;
+ dump("SQL: " + sql);
+ let stmt = this._datastore._createStatement(sql);
+ dump("created\n");
+ let highId = 0;
+ if (stmt.step()) {
+ dump("stepped, retrieving\n");
+ highId = stmt.row["m_id"];
+ }
+ stmt.reset();
+
+ return highId;
+ },
+ */
+
+ select: function(aColName, aColValue, aCallbackThis, aCallback) {
+ let stmt;
+ if (!(aColName in this._stmtCache)) {
+ let sqlString = "SELECT * FROM " + this._tableDef._realName;
+ if (aColName)
+ sqlString += " WHERE " + aColName + " = :value";
+ stmt = this._datastore._createAsyncStatement(sqlString);
+ this._stmtCache[aColName] = stmt;
+ }
+ else
+ stmt = this._stmtCache[aColName];
+
+ if (aColName)
+ this._datastore._bindVariant(stmt, 0, aColValue);
+ // so, we're tricky-like and lazy and actually return the row, so we don't
+ // want to reset until the user tries to use the statement again, as I
+ // fear we would otherwise lose our awesome row binding (and have to copy
+ // it, etc.)
+ stmt.executeAsync(new DatabindCallback(this, aCallbackThis, aCallback));
+ },
+
+ insert: function(aValueDicts) {
+ let stmt = this._insertStmt;
+ for each (let [,valueDict] in Iterator(aValueDicts)) {
+ for each (let [iColDef, colDef] in Iterator(this._tableDef.columns)) {
+ this._log.debug("insert arg: " + colDef[0] + "=" + valueDict[colDef[0]]);
+ stmt.params[colDef[0]] = valueDict[colDef[0]];
+ }
+ stmt.executeAsync(this._datastore.trackAsync());
+ }
+ }
+};
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/datamodel.js
@@ -0,0 +1,470 @@
+/* ***** 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 Thunderbird Global Database.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Messaging, Inc.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Andrew Sutherland <asutherland@asutherland.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 ***** */
+
+EXPORTED_SYMBOLS = ["GlodaAttributeDBDef",
+ "GlodaConversation", "GlodaFolder", "GlodaMessage",
+ "GlodaContact", "GlodaIdentity"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gloda/modules/log4moz.js");
+const LOG = Log4Moz.Service.getLogger("gloda.datamodel");
+
+Cu.import("resource://gloda/modules/utils.js");
+
+/**
+ * @class Represents a gloda attribute definition's DB form. This class
+ * stores the information in the database relating to this attribute
+ * definition. Access its attrDef attribute to get at the realy juicy data.
+ * This main interesting thing this class does is serve as the keeper of the
+ * mapping from parameters to attribute ids in the database if this is a
+ * parameterized attribute.
+ */
+function GlodaAttributeDBDef(aDatastore, aID, aCompoundName, aAttrType,
+ aPluginName, aAttrName) {
+ this._datastore = aDatastore;
+ this._id = aID;
+ this._compoundName = aCompoundName;
+ this._attrType = aAttrType;
+ this._pluginName = aPluginName;
+ this._attrName = aAttrName;
+
+ this.attrDef = null;
+
+ /** Map parameter values to the underlying database id. */
+ this._parameterBindings = {};
+}
+
+GlodaAttributeDBDef.prototype = {
+ get id() { return this._id; },
+ get attributeName() { return this._attrName; },
+
+ get parameterBindings() { return this._parameterBindings; },
+
+ /**
+ * Bind a parameter value to the attribute definition, allowing use of the
+ * attribute-parameter as an attribute.
+ *
+ * @return
+ */
+ bindParameter: function gloda_attr_bindParameter(aValue) {
+ // people probably shouldn't call us with null, but handle it
+ if (aValue == null) {
+ return this._id;
+ }
+ if (aValue in this._parameterBindings) {
+ return this._parameterBindings[aValue];
+ }
+ // no database entry exists if we are here, so we must create it...
+ let id = this._datastore._createAttributeDef(this._attrType,
+ this._pluginName, this._attrName, aValue);
+ this._parameterBindings[aValue] = id;
+ this._datastore.reportBinding(id, this, aValue);
+ return id;
+ },
+
+ /**
+ * Given a list of values (if non-singular) or a single value (if singular),
+ * return a list (regardless of plurality) of database-ready [attribute id,
+ * value] tuples. This is intended to be used to directly convert the value
+ * of a property on an object that corresponds to a bound attribute.
+ */
+ convertValuesToDBAttributes:
+ function gloda_attr_convertValuesToDBAttributes(aInstanceValues) {
+ let nounDef = this.attrDef.objectNounDef;
+ if (this._singular) {
+ if (nounDef.usesParameter) {
+ let [param, dbValue] = nounDef.toParamAndValue(aInstanceValues);
+ return [[this.bindParameter(param), dbValue]];
+ }
+ else {
+ return [[this._id, nounDef.toParamAndValue(aInstanceValues)[1]]];
+ }
+ }
+ else {
+ let dbAttributes = [];
+ if (nounDef.usesParameter) {
+ for each (let [, instanceValue] in Iterator(aInstanceValues)) {
+ let [param, dbValue] = nounDef.toParamAndValue(instanceValue);
+ dbAttributes.push([this.bindParameter(param), dbValue]);
+ }
+ }
+ else {
+ for each (let [, instanceValue] in Iterator(aInstanceValues)) {
+ dbAttributes.push([this._id,
+ nounDef.toParamAndValue(instanceValue)[1]]);
+ }
+ }
+ return dbAttributes;
+ }
+ },
+
+ toString: function() {
+ return this._compoundName;
+ }
+};
+
+let GlodaHasAttributesMixIn = {
+ enumerateAttributes: function gloda_attrix_enumerateAttributes() {
+ let nounDef = this.NOUN_DEF;
+ for each (let [key, value] in Iterator(this)) {
+ let attrDef = nounDef.attribsByBoundName[key];
+ // we expect to not have attributes for underscore prefixed values (those
+ // are managed by the instance's logic. we also want to not explode
+ // should someone crap other values in there, we get both birds with this
+ // one stone.
+ if (attrDef === undefined)
+ continue;
+ if (attrDef.singular) {
+ // ignore attributes with null values
+ if (value != null)
+ yield [attrDef, [value]];
+ }
+ else {
+ // ignore attributes with no values
+ if (value.length)
+ yield [attrDef, value];
+ }
+ }
+ },
+
+ domContribute: function gloda_attrix_domContribute(aDomNode) {
+ let nounDef = this.NOUN_DEF;
+ for each (let [attrName, attr] in
+ Iterator(nounDef.domExposeAttribsByBoundName)) {
+ if (this[attrName])
+ aDomNode.setAttribute(attr.domExpose, this[attrName]);
+ }
+ },
+};
+
+function MixIn(aConstructor, aMixIn) {
+ let proto = aConstructor.prototype;
+ for (let [name, func] in Iterator(aMixIn)) {
+ if (name.substring(0, 4) == "get_")
+ proto.__defineGetter__(name.substring(4), func);
+ else
+ proto[name] = func;
+ }
+}
+
+/**
+ * @class A gloda conversation (thread) exists so that messages can belong.
+ */
+function GlodaConversation(aDatastore, aID, aSubject, aOldestMessageDate,
+ aNewestMessageDate) {
+ this._datastore = aDatastore;
+ this._id = aID;
+ this._subject = aSubject;
+ this._oldestMessageDate = aOldestMessageDate;
+ this._newestMessageDate = aNewestMessageDate;
+}
+
+GlodaConversation.prototype = {
+ NOUN_ID: 101,
+ get id() { return this._id; },
+ get subject() { return this._subject; },
+ get oldestMessageDate() { return this._oldestMessageDate; },
+ get newestMessageDate() { return this._newestMessageDate; },
+
+ getMessagesCollection: function gloda_conversation_getMessagesCollection(
+ aListener, aData) {
+ let query = new GlodaMessage.prototype.NOUN_DEF.queryClass();
+ query.conversation(this._id).orderBy("date");
+ return query.getCollection(aListener, aData);
+ },
+
+ toString: function gloda_conversation_toString() {
+ return "Conversation:" + this._id;
+ },
+};
+
+function GlodaFolder(aDatastore, aID, aURI, aDirtyStatus, aPrettyName) {
+ this._datastore = aDatastore;
+ this._id = aID;
+ this._uri = aURI;
+ this._dirtyStatus = aDirtyStatus;
+ this._prettyName = aPrettyName;
+}
+
+GlodaFolder.prototype = {
+ NOUN_ID: 100,
+ /** The folder is believed to be up-to-date */
+ kFolderClean: 0,
+ /** The folder has some un-indexed or dirty messages */
+ kFolderDirty: 1,
+ /** The folder needs to be entirely re-indexed, regardless of the flags on
+ * the messages in the folder. This state will be downgraded to dirty */
+ kFolderFilthy: 2,
+ get id() { return this._id; },
+ get uri() { return this._uri; },
+ get dirtyStatus() { return this._dirtyStatus; },
+ set dirtyStatus(aNewStatus) {
+ if (aNewStatus != this._dirtyStatus) {
+ this._dirtyStatus = aNewStatus;
+ this._datastore.updateFolderDirtyStatus(this);
+ }
+ },
+ get name() { return this._prettyName; },
+ toString: function gloda_folder_toString() {
+ return "Folder:" + this._id;
+ }
+}
+
+/**
+ * @class A message representation.
+ */
+function GlodaMessage(aDatastore, aID, aFolderID, aMessageKey,
+ aConversationID, aConversation, aDate,
+ aHeaderMessageID, aDeleted, aJsonText) {
+ this._datastore = aDatastore;
+ this._id = aID;
+ this._folderID = aFolderID;
+ this._messageKey = aMessageKey;
+ this._conversationID = aConversationID;
+ this._conversation = aConversation;
+ this._date = aDate;
+ this._headerMessageID = aHeaderMessageID;
+ if (aJsonText)
+ this._jsonText = aJsonText;
+
+ // only set _deleted if we're deleted, otherwise the undefined does our
+ // speaking for us.
+ if (aDeleted)
+ this._deleted = aDeleted;
+}
+
+GlodaMessage.prototype = {
+ NOUN_ID: 102,
+ get id() { return this._id; },
+ get folderID() { return this._folderID; },
+ get messageKey() { return this._messageKey; },
+ get conversationID() { return this._conversationID; },
+ // conversation is special
+ get headerMessageID() { return this._headerMessageID; },
+
+ get date() { return this._date; },
+ set date(aNewDate) { this._date = aNewDate; },
+
+ get folderURI() {
+ if (this._folderID != null)
+ return this._datastore._mapFolderID(this._folderID).uri;
+ else
+ return null;
+ },
+ get conversation() {
+ return this._conversation;
+ },
+
+ toString: function gloda_message_toString() {
+ // uh, this is a tough one...
+ return "Message:" + this._id;
+ },
+
+ _clone: function gloda_message_clone() {
+ return new GlodaMessage(this._datastore, this._id, this._folderID,
+ this._messageKey, this._conversationID, this._conversation, this._date,
+ this._headerMessageID, this._deleted);
+ },
+
+ _ghost: function gloda_message_ghost() {
+ this._folderID = null;
+ this._messageKey = null;
+ },
+
+ _nuke: function gloda_message_nuke() {
+ this._id = null;
+ this._folderID = null;
+ this._messageKey = null;
+ this._conversationID = null;
+ this._conversation = null;
+ this.date = null;
+ this._headerMessageID = null;
+
+ this._datastore = null;
+ },
+
+ /**
+ * Return the underlying nsIMsgDBHdr from the folder storage for this, or
+ * null if the message does not exist for one reason or another.
+ * This method no longer caches the result, so it's up to you.
+ */
+ get folderMessage() {
+ if (this._folderID === null || this._messageKey === null)
+ return null;
+ let rdfService = Cc['@mozilla.org/rdf/rdf-service;1'].
+ getService(Ci.nsIRDFService);
+ let folder = rdfService.GetResource(
+ this._datastore._mapFolderID(this._folderID).uri);
+ if (folder instanceof Ci.nsIMsgFolder) {
+ let folderMessage = folder.GetMessageHeader(this._messageKey);
+ if (folderMessage !== null) {
+ // verify the message-id header matches what we expect...
+ if (folderMessage.messageId != this._headerMessageID) {
+ LOG.info("Message with message key does not match expected " +
+ "header! (" + this._headerMessageID + " expected, got " +
+ folderMessage.messageId + ")");
+ folderMessage = null;
+ }
+ }
+ return folderMessage;
+ }
+
+ // this only gets logged if things have gone very wrong. we used to throw
+ // here, but it's unlikely our caller can do anything more meaningful than
+ // treating this as a disappeared message.
+ LOG.info("Unable to locate folder message for: " + this._folderID + ":" +
+ this._messageKey);
+ return null;
+ },
+ get folderMessageURI() {
+ let folderMessage = this.folderMessage;
+ if (folderMessage)
+ return folderMessage.folder.getUriForMsg(folderMessage);
+ else
+ return null;
+ }
+};
+MixIn(GlodaMessage, GlodaHasAttributesMixIn);
+
+/**
+ * @class Contacts correspond to people (one per person), and may own multiple
+ * identities (e-mail address, IM account, etc.)
+ */
+function GlodaContact(aDatastore, aID, aDirectoryUUID, aContactUUID, aName,
+ aPopularity, aFrecency, aJsonText) {
+ this._datastore = aDatastore;
+ this._id = aID;
+ this._directoryUUID = aDirectoryUUID;
+ this._contactUUID = aContactUUID;
+ this._name = aName;
+ this._popularity = aPopularity;
+ this._frecency = aFrecency;
+ if (aJsonText)
+ this._jsonText = aJsonText;
+
+ this._identities = null;
+}
+
+GlodaContact.prototype = {
+ NOUN_ID: 103,
+
+ get id() { return this._id; },
+ get directoryUUID() { return this._directoryUUID; },
+ get contactUUID() { return this._contactUUID; },
+ get name() { return this._name; },
+ set name(aName) { this._name = aName; },
+
+ get popularity() { return this._popularity; },
+ set popularity(aPopularity) {
+ this._popularity = aPopularity;
+ this.dirty = true;
+ },
+
+ get frecency() { return this._frecency; },
+ set frecency(aFrecency) {
+ this._frecency = aFrecency;
+ this.dirty = true;
+ },
+
+ get identities() {
+ return this._identities;
+ },
+
+ toString: function gloda_contact_toString() {
+ return "Contact:" + this._id;
+ },
+
+ get accessibleLabel() {
+ return "Contact: " + this._name;
+ },
+
+ _clone: function gloda_contact_clone() {
+ return new GlodaContact(this._datastore, this._id, this._directoryUUID,
+ this._contactUUID, this._name, this._popularity, this._frecency);
+ },
+};
+MixIn(GlodaContact, GlodaHasAttributesMixIn);
+
+
+/**
+ * @class A specific means of communication for a contact.
+ */
+function GlodaIdentity(aDatastore, aID, aContactID, aContact, aKind, aValue,
+ aDescription, aIsRelay) {
+ this._datastore = aDatastore;
+ this._id = aID;
+ this._contactID = aContactID;
+ this._contact = aContact;
+ this._kind = aKind;
+ this._value = aValue;
+ this._description = aDescription;
+ this._isRelay = aIsRelay;
+}
+
+GlodaIdentity.prototype = {
+ NOUN_ID: 104,
+ get id() { return this._id; },
+ get contactID() { return this._contactID; },
+ get contact() { return this._contact; },
+ get kind() { return this._kind; },
+ get value() { return this._value; },
+ get description() { return this._description; },
+ get isRelay() { return this._isRelay; },
+
+ get uniqueValue() {
+ return this._kind + "@" + this._value;
+ },
+
+ toString: function gloda_identity_toString() {
+ return "Identity:" + this._kind + ":" + this._value;
+ },
+
+ get abCard() {
+ return GlodaUtils.getCardForEmail(this._value);
+ },
+
+ pictureURL: function(aSize) {
+ let md5hash = GlodaUtils.md5HashString(this._value);
+ let gravURL = "http://www.gravatar.com/avatar/" + md5hash +
+ "?d=identicon&s=" + aSize + "&r=g";
+ return gravURL;
+ }
+};
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/datastore.js
@@ -0,0 +1,3135 @@
+/* ***** 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 Thunderbird Global Database.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Messaging, Inc.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Andrew Sutherland <asutherland@asutherland.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 ***** */
+
+/* This file looks to Myk Melez <myk@mozilla.org>'s Mozilla Labs snowl
+ * project's (http://hg.mozilla.org/labs/snowl/) modules/datastore.js
+ * for inspiration and idioms (and also a name :).
+ */
+
+EXPORTED_SYMBOLS = ["GlodaDatastore"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gloda/modules/log4moz.js");
+
+Cu.import("resource://gloda/modules/datamodel.js");
+Cu.import("resource://gloda/modules/databind.js");
+Cu.import("resource://gloda/modules/collection.js");
+
+let MBM_LOG = Log4Moz.Service.getLogger("gloda.ds.mbm");
+
+/**
+ * @class This callback handles processing the asynchronous query results of
+ * GlodaDatastore.getMessagesByMessageID. Because that method is only
+ * called as part of the indexing process, we are guaranteed that there will
+ * be no real caching ramifications. Accordingly, we can also defer our cache
+ * processing (via GlodaCollectionManager) until the query completes.
+ *
+ * @param aMsgIDToIndex Map from message-id to the desired
+ *
+ * @constructor
+ */
+function MessagesByMessageIdCallback(aMsgIDToIndex, aResults,
+ aCallback, aCallbackThis) {
+ this.msgIDToIndex = aMsgIDToIndex;
+ this.results = aResults;
+ this.callback = aCallback;
+ this.callbackThis = aCallbackThis;
+}
+
+MessagesByMessageIdCallback.prototype = {
+ onItemsAdded: function gloda_ds_mbmi_onItemsAdded(aItems, aCollection) {
+ MBM_LOG.debug("getting results...");
+ for each (let [, message] in Iterator(aItems)) {
+ this.results[this.msgIDToIndex[message.headerMessageID]].push(message);
+ }
+ },
+ onItemsModified: function () {},
+ onItemsRemoved: function () {},
+ onQueryCompleted: function gloda_ds_mbmi_onQueryCompleted(aCollection) {
+ MBM_LOG.debug("query completed, notifying... " + this.results);
+ // we no longer need to unify; it is done for us.
+
+ this.callback.call(this.callbackThis, this.results);
+ }
+};
+
+let PCH_LOG = Log4Moz.Service.getLogger("gloda.ds.pch");
+
+function PostCommitHandler(aCallbacks) {
+ this.callbacks = aCallbacks;
+ GlodaDatastore._pendingAsyncStatements++;
+}
+
+PostCommitHandler.prototype = {
+ handleResult: function gloda_ds_pch_handleResult(aResultSet) {
+ },
+
+ handleError: function gloda_ds_pch_handleError(aError) {
+ PCH_LOG.error("database error:" + aError)
+ },
+
+ handleCompletion: function gloda_ds_pch_handleCompletion(aReason) {
+ if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ for each (let [iCallback, callback] in Iterator(this.callbacks)) {
+ try {
+ callback();
+ }
+ catch (ex) {
+ dump("PostCommitHandler callback (" + ex.fileName + ":" +
+ ex.lineNumber + ") threw: " + ex);
+ }
+ }
+ }
+ GlodaDatastore._asyncCompleted();
+ }
+};
+
+let QFQ_LOG = Log4Moz.Service.getLogger("gloda.ds.qfq");
+
+let QueryFromQueryResolver = {
+ onItemsAdded: function(aIgnoredItems, aCollection, aFake) {
+ let originColl = aCollection.dataStack ? aCollection.dataStack.pop()
+ : aCollection.data;
+ if (aCollection.completionShifter)
+ aCollection.completionShifter.push(originColl);
+ else
+ aCollection.completionShifter = [originColl];
+
+ if (!aFake) {
+ originColl.deferredCount--;
+ originColl.resolvedCount++;
+ }
+
+ // bail if we are still pending on some other load completion
+ if (originColl.deferredCount > 0) {
+ //QFQ_LOG.debug("QFQR: bailing " + originColl._nounDef.name);
+ return;
+ }
+
+ let referencesByNounID = originColl.masterCollection.referencesByNounID;
+ let inverseReferencesByNounID =
+ originColl.masterCollection.inverseReferencesByNounID;
+
+ if (originColl.pendingItems) {
+ for (let [, item] in Iterator(originColl.pendingItems)) {
+ //QFQ_LOG.debug("QFQR: loading deferred " + item.NOUN_ID + ":" + item.id);
+ GlodaDatastore.loadNounDeferredDeps(item, referencesByNounID,
+ inverseReferencesByNounID);
+ }
+
+ // we need to consider the possibility that we are racing a collection very
+ // much like our own. as such, this means we need to perform cache
+ // unification as our last step.
+ GlodaCollectionManager.cacheLoadUnify(originColl._nounDef.id,
+ originColl.pendingItems, false);
+
+ // just directly tell the collection about the items. we know the query
+ // matches (at least until we introduce predicates that we cannot express
+ // in SQL.)
+ //QFQ_LOG.debug(" QFQR: about to trigger listener: " + originColl._listener +
+ // "with collection: " + originColl._nounDef.name);
+ originColl._onItemsAdded(originColl.pendingItems);
+ delete originColl.pendingItems;
+ delete originColl._pendingIdMap;
+ }
+ },
+ onItemsModified: function() {
+ },
+ onItemsRemoved: function() {
+ },
+ onQueryCompleted: function(aCollection) {
+ let originColl = aCollection.completionShifter ?
+ aCollection.completionShifter.shift() : aCollection.data;
+ //QFQ_LOG.debug(" QFQR about to trigger completion with collection: " +
+ // originColl._nounDef.name);
+ if (originColl.deferredCount <= 0) {
+ originColl._onQueryCompleted();
+ }
+ },
+};
+
+/**
+ * @class Handles the results from a GlodaDatastore.queryFromQuery call.
+ * @constructor
+ */
+function QueryFromQueryCallback(aStatement, aNounDef, aCollection) {
+ this.statement = aStatement;
+ this.nounDef = aNounDef;
+ this.collection = aCollection;
+
+ //QFQ_LOG.debug("Creating QFQCallback for noun: " + aNounDef.name);
+
+ // the master collection holds the referencesByNounID
+ this.referencesByNounID = {};
+ this.masterReferencesByNounID =
+ this.collection.masterCollection.referencesByNounID;
+ this.inverseReferencesByNounID = {};
+ this.masterInverseReferencesByNounID =
+ this.collection.masterCollection.inverseReferencesByNounID;
+ // we need to contribute our references as we load things; we need this
+ // because of the potential for circular dependencies and our inability to
+ // put things into the caching layer (or collection's _idMap) until we have
+ // fully resolved things.
+ if (this.nounDef.id in this.masterReferencesByNounID)
+ this.selfReferences = this.masterReferencesByNounID[this.nounDef.id];
+ else
+ this.selfReferences = this.masterReferencesByNounID[this.nounDef.id] = {};
+ if (this.nounDef.parentColumnAttr) {
+ if (this.nounDef.id in this.masterInverseReferencesByNounID)
+ this.selfInverseReferences =
+ this.masterInverseReferencesByNounID[this.nounDef.id];
+ else
+ this.selfInverseReferences =
+ this.masterInverseReferencesByNounID[this.nounDef.id] = {};
+ }
+
+ this.needsLoads = false;
+
+ GlodaDatastore._pendingAsyncStatements++;
+}
+
+QueryFromQueryCallback.prototype = {
+ handleResult: function gloda_ds_qfq_handleResult(aResultSet) {
+ let pendingItems = this.collection.pendingItems;
+ let pendingIdMap = this.collection._pendingIdMap;
+ let row;
+ let nounDef = this.nounDef;
+ let nounID = nounDef.id;
+ while (row = aResultSet.getNextRow()) {
+ let item = nounDef.objFromRow.call(nounDef.datastore, row);
+ // try and replace the item with one from the cache, if we can
+ let cachedItem = GlodaCollectionManager.cacheLookupOne(nounID, item.id,
+ false);
+
+ // if we already have a copy in the pending id map, skip it
+ if (item.id in pendingIdMap)
+ continue;
+
+ //QFQ_LOG.debug("loading item " + nounDef.id + ":" + item.id + " existing: " +
+ // this.selfReferences[item.id] + " cached: " + cachedItem);
+ if (cachedItem)
+ item = cachedItem;
+ // we may already have been loaded by this process
+ else if (this.selfReferences[item.id] != null)
+ item = this.selfReferences[item.id];
+ // perform loading logic which may produce reference dependencies
+ else
+ this.needsLoads =
+ GlodaDatastore.loadNounItem(item, this.referencesByNounID,
+ this.inverseReferencesByNounID) ||
+ this.needsLoads;
+
+ // add ourself to the references by our id
+ // QFQ_LOG.debug("saving item " + nounDef.id + ":" + item.id + " to self-refs");
+ this.selfReferences[item.id] = item;
+
+ // if we're tracking it, add ourselves to our parent's list of children
+ // too
+ if (this.selfInverseReferences) {
+ let parentID = item[nounDef.parentColumnAttr.idStorageAttributeName];
+ let childrenList = this.selfInverseReferences[parentID];
+ if (childrenList === undefined)
+ childrenList = this.selfInverseReferences[parentID] = [];
+ childrenList.push(item);
+ }
+
+ pendingItems.push(item);
+ pendingIdMap[item.id] = item;
+ }
+ },
+
+ handleError: function gloda_ds_qfq_handleError(aError) {
+ GlodaDatastore._log.error("Async queryFromQuery error: " +
+ aError.result + ": " + aError.message);
+ },
+
+ handleCompletion: function gloda_ds_qfq_handleCompletion(aReason) {
+ this.statement.finalize();
+ this.statement = null;
+
+ //QFQ_LOG.debug("handleCompletion: " + this.collection._nounDef.name);
+
+ if (this.needsLoads) {
+ for each (let [nounID, references] in Iterator(this.referencesByNounID)) {
+ if (nounID == this.nounDef.id)
+ continue;
+ let nounDef = GlodaDatastore._nounIDToDef[nounID];
+ //QFQ_LOG.debug(" have references for noun: " + nounDef.name);
+ // try and load them out of the cache/existing collections. items in the
+ // cache will be fully formed, which is nice for us.
+ // XXX this mechanism will get dubious when we have multiple paths to a
+ // single noun-type. For example, a -> b -> c, a-> c; two paths to c
+ // and we're looking at issuing two requests to c, the latter of which
+ // will be a superset of the first one. This does not currently pose
+ // a problem because we only have a -> b -> c -> b, and sequential
+ // processing means no alarms and no surprises.
+ let masterReferences = this.masterReferencesByNounID[nounID];
+ if (masterReferences === undefined)
+ masterReferences = this.masterReferencesByNounID[nounID] = {};
+ let outReferences;
+ if (nounDef.parentColumnAttr)
+ outReferences = {};
+ else
+ outReferences = masterReferences;
+ let [foundCount, notFoundCount, notFound] =
+ GlodaCollectionManager.cacheLookupMany(nounDef.id, references,
+ outReferences);
+
+ if (nounDef.parentColumnAttr) {
+ let inverseReferences;
+ if (nounDef.id in this.masterInverseReferencesByNounID)
+ inverseReferences =
+ this.masterInverseReferencesByNounID[nounDef.id];
+ else
+ inverseReferences =
+ this.masterInverseReferencesByNounID[nounDef.id] = {};
+
+ for each (let item in outReferences) {
+ masterReferences[item.id] = item;
+ let parentID = item[nounDef.parentColumnAttr.idStorageAttributeName];
+ let childrenList = inverseReferences[parentID];
+ if (childrenList === undefined)
+ childrenList = inverseReferences[parentID] = [];
+ childrenList.push(item);
+ }
+ }
+
+ //QFQ_LOG.debug(" found: " + foundCount + " not found: " + notFoundCount);
+ if (notFoundCount === 0) {
+ this.collection.resolvedCount++;
+ }
+ else {
+ this.collection.deferredCount++;
+ let query = new nounDef.queryClass();
+ query.id.apply(query, [id for (id in notFound)]);
+
+ this.collection.masterCollection.subCollections[nounDef.id] =
+ GlodaDatastore.queryFromQuery(query, QueryFromQueryResolver,
+ this.collection,
+ // we fully expect/allow for there being no such subcollection yet.
+ this.collection.masterCollection.subCollections[nounDef.id],
+ this.collection.masterCollection);
+ }
+ }
+
+ for each (let [nounID, inverseReferences] in
+ Iterator(this.inverseReferencesByNounID)) {
+ this.collection.deferredCount++;
+ let nounDef = GlodaDatastore._nounIDToDef[nounID];
+
+ //QFQ_LOG.debug("Want to load inverse via " + nounDef.parentColumnAttr.boundName);
+
+ let query = new nounDef.queryClass();
+ // we want to constrain using the parent column
+ let queryConstrainer = query[nounDef.parentColumnAttr.boundName];
+ queryConstrainer.apply(query, [pid for (pid in inverseReferences)]);
+ this.collection.masterCollection.subCollections[nounDef.id] =
+ GlodaDatastore.queryFromQuery(query, QueryFromQueryResolver,
+ this.collection,
+ // we fully expect/allow for there being no such subcollection yet.
+ this.collection.masterCollection.subCollections[nounDef.id],
+ this.collection.masterCollection);
+ }
+ }
+ else {
+ this.collection.deferredCount--;
+ this.collection.resolvedCount++;
+ }
+
+ //QFQ_LOG.debug(" defer: " + this.collection.deferredCount +
+ // " resolved: " + this.collection.resolvedCount);
+
+ // process immediately and kick-up to the master collection...
+ try {
+ if (this.collection.deferredCount <= 0) {
+ // this guy will resolve everyone using referencesByNounID and issue the
+ // call to this.collection._onItemsAdded to propagate things to the
+ // next concerned subCollection or the actual listener if this is the
+ // master collection. (Also, call _onQueryCompleted).
+ QueryFromQueryResolver.onItemsAdded(null, {data: this.collection}, true);
+ QueryFromQueryResolver.onQueryCompleted({data: this.collection});
+ }
+ }
+ finally {
+ GlodaDatastore._asyncCompleted();
+ }
+ }
+};
+
+
+/**
+ * Database abstraction layer. Contains explicit SQL schemas for our
+ * fundamental representations (core 'nouns', if you will) as well as
+ * specialized functions for then dealing with each type of object. At the
+ * same time, we are beginning to support extension-provided tables, which
+ * call into question whether we really need our hand-rolled code, or could
+ * simply improve the extension-provided table case to work for most of our
+ * hand-rolled cases.
+ * For now, the argument can probably be made that our explicit schemas and code
+ * is readable/intuitive (not magic) and efficient (although generic stuff
+ * could also be made efficient, if slightly evil through use of eval or some
+ * other code generation mechanism.)
+ *
+ * === Data Model Interaction / Dependencies
+ *
+ * Dependent on and assumes limited knowledge of the datamodel.js
+ * implementations. datamodel.js actually has an implicit dependency on
+ * our implementation, reaching back into the datastore via the _datastore
+ * attribute which we pass into every instance we create.
+ * We pass a reference to ourself as we create the datamodel.js instances (and
+ * they store it as _datastore) because of a half-implemented attempt to make
+ * it possible to live in a world where we have multiple datastores. This
+ * would be desirable in the cases where we are dealing with multiple SQLite
+ * databases. This could be because of per-account global databases or
+ * some other segmentation. This was abandoned when the importance of
+ * per-account databases was diminished following public discussion, at least
+ * for the short-term, but no attempted was made to excise the feature or
+ * preclude it. (Merely a recognition that it's too much to try and implement
+ * correct right now, especially because our solution might just be another
+ * (aggregating) layer on top of things, rather than complicating the lower
+ * levels.)
+ *
+ * === Object Identity / Caching
+ *
+ * The issue of object identity is handled by integration with the collection.js
+ * provided GlodaCollectionManager. By "Object Identity", I mean that we only
+ * should ever have one object instance alive at a time that corresponds to
+ * an underlying database row in the database. Where possible we avoid
+ * performing database look-ups when we can check if the object is already
+ * present in memory; in practice, this means when we are asking for an object
+ * by ID. When we cannot avoid a database query, we attempt to make sure that
+ * we do not return a duplicate object instance, instead replacing it with the
+ * 'live' copy of the object. (Ideally, we would avoid any redundant
+ * construction costs, but that is not currently the case.)
+ * Although you should consult the GlodaCollectionManager for details, the
+ * general idea is that we have 'collections' which represent views of the
+ * database (based on a query) which use a single mechanism for double duty.
+ * The collections are registered with the collection manager via weak
+ * reference. The first 'duty' is that since the collections may be desired
+ * to be 'live views' of the data, we want them to update as changes occur.
+ * The weak reference allows the collection manager to track the 'live'
+ * collections and update them. The second 'duty' is the caching/object
+ * identity duty. In theory, every live item should be referenced by at least
+ * one collection, making it reachable for object identity/caching purposes.
+ * There is also an explicit (inclusive) caching layer present to both try and
+ * avoid poor performance from some of the costs of this strategy, as well as
+ * to try and keep track of objects that are being worked with that are not
+ * (yet) tracked by a collection. Using a size-bounded cache is clearly not
+ * a guarantee of correctness for this, but is suspected will work quite well.
+ * (Well enough to be dangerous because the inevitable failure case will not be
+ * expected.)
+ *
+ * The current strategy may not be the optimal one, feel free to propose and/or
+ * implement better ones, especially if you have numbers.
+ * The current strategy is not fully implemented in this file, but the common
+ * cases are believed to be covered. (Namely, we fail to purge items from the
+ * cache as they are purged from the database.)
+ *
+ * === Things That May Not Be Obvious (Gotchas)
+ *
+ * Although the schema includes "triggers", they are currently not used
+ * and were added when thinking about implementing the feature. We will
+ * probably implement this feature at some point, which is why they are still
+ * in there.
+ *
+ * We, and the layers above us, are not sufficiently thorough at cleaning out
+ * data from the database, and may potentially orphan it _as new functionality
+ * is added in the future at layers above us_. That is, currently we should
+ * not be leaking database rows, but we may in the future. This is because
+ * we/the layers above us lack a mechanism to track dependencies based on
+ * attributes. Say a plugin exists that extracts recipes from messages and
+ * relates them via an attribute. To do so, it must create new recipe rows
+ * in its own table as new recipes are discovered. No automatic mechanism
+ * will purge recipes as their source messages are purged, nor does any
+ * event-driven mechanism explicitly inform the plugin. (It could infer
+ * such an event from the indexing/attribute-providing process, or poll the
+ * states of attributes to accomplish this, but that is not desirable.) This
+ * needs to be addressed, and may be best addressed at layers above
+ * datastore.js.
+ * @namespace
+ */
+var GlodaDatastore = {
+ _log: null,
+
+ /* see Gloda's documentation for these constants */
+ kSpecialNotAtAll: 0,
+ kSpecialColumn: 16,
+ kSpecialColumnChildren: 16|1,
+ kSpecialColumnParent: 16|2,
+ kSpecialString: 32,
+ kSpecialFulltext: 64,
+
+ kConstraintIdIn: 0,
+ kConstraintIn: 1,
+ kConstraintRanges: 2,
+ kConstraintEquals: 3,
+ kConstraintStringLike: 4,
+ kConstraintFulltext: 5,
+
+ /* ******************* SCHEMA ******************* */
+
+ _schemaVersion: 10,
+ _schema: {
+ tables: {
+
+ // ----- Messages
+ folderLocations: {
+ columns: [
+ "id INTEGER PRIMARY KEY",
+ "folderURI TEXT NOT NULL",
+ "dirtyStatus INTEGER NOT NULL",
+ "name TEXT NOT NULL",
+ ],
+
+ triggers: {
+ delete: "DELETE from messages WHERE folderID = OLD.id",
+ },
+ },
+
+ conversations: {
+ columns: [
+ "id INTEGER PRIMARY KEY",
+ "subject TEXT NOT NULL",
+ "oldestMessageDate INTEGER",
+ "newestMessageDate INTEGER",
+ ],
+
+ indices: {
+ subject: ['subject'],
+ oldestMessageDate: ['oldestMessageDate'],
+ newestMessageDate: ['newestMessageDate'],
+ },
+
+ fulltextColumns: [
+ "subject TEXT",
+ ],
+
+ triggers: {
+ delete: "DELETE from messages WHERE conversationID = OLD.id",
+ },
+ },
+
+ /**
+ * A message record correspond to an actual message stored in a folder
+ * somewhere, or is a ghost record indicating a message that we know
+ * should exist, but which we have not seen (and which we may never see).
+ * We represent these ghost messages by storing NULL values in the
+ * folderID and messageKey fields; this may need to change to other
+ * sentinel values if this somehow impacts performance.
+ */
+ messages: {
+ columns: [
+ "id INTEGER PRIMARY KEY",
+ "folderID INTEGER REFERENCES folderLocations(id)",
+ "messageKey INTEGER",
+ "conversationID INTEGER NOT NULL REFERENCES conversations(id)",
+ "date INTEGER",
+ // we used to have the parentID, but because of the very real
+ // possibility of multiple copies of a message with a given
+ // message-id, the parentID concept is unreliable.
+ "headerMessageID TEXT",
+ "deleted INTEGER NOT NULL default 0",
+ "jsonAttributes TEXT",
+ ],
+
+ indices: {
+ messageLocation: ['folderID', 'messageKey'],
+ headerMessageID: ['headerMessageID'],
+ conversationID: ['conversationID'],
+ date: ['date'],
+ deleted: ['deleted'],
+ },
+
+ fulltextColumns: [
+ "subject TEXT",
+ "body TEXT",
+ "attachmentNames TEXT",
+ ],
+
+ triggers: {
+ delete: "DELETE FROM messageAttributes WHERE messageID = OLD.id",
+ },
+ },
+
+ // ----- Attributes
+ attributeDefinitions: {
+ columns: [
+ "id INTEGER PRIMARY KEY",
+ "attributeType INTEGER NOT NULL",
+ "extensionName TEXT NOT NULL",
+ "name TEXT NOT NULL",
+ "parameter BLOB",
+ ],
+
+ triggers: {
+ delete: "DELETE FROM messageAttributes WHERE attributeID = OLD.id",
+ },
+ },
+
+ messageAttributes: {
+ columns: [
+ "conversationID INTEGER NOT NULL REFERENCES conversations(id)",
+ "messageID INTEGER NOT NULL REFERENCES messages(id)",
+ "attributeID INTEGER NOT NULL REFERENCES attributeDefinitions(id)",
+ "value NUMERIC",
+ ],
+
+ indices: {
+ attribQuery: [
+ "attributeID", "value",
+ /* covering: */ "conversationID", "messageID"],
+ },
+ },
+
+ // ----- Contacts / Identities
+
+ /**
+ * Corresponds to a human being and roughly to an address book entry.
+ * Constrast with an identity, which is a specific e-mail address, IRC
+ * nick, etc. Identities belong to contacts, and this relationship is
+ * expressed on the identityAttributes table.
+ */
+ contacts: {
+ columns: [
+ "id INTEGER PRIMARY KEY",
+ "directoryUUID TEXT",
+ "contactUUID TEXT",
+ "popularity INTEGER",
+ "frecency INTEGER",
+ "name TEXT",
+ "jsonAttributes TEXT",
+ ],
+ indices: {
+ popularity: ["popularity"],
+ frecency: ["frecency"],
+ },
+ },
+
+ contactAttributes: {
+ columns: [
+ "contactID INTEGER NOT NULL REFERENCES contacts(id)",
+ "attributeID INTEGER NOT NULL REFERENCES attributeDefinitions(id)",
+ "value NUMERIC"
+ ],
+ indices: {
+ contactAttribQuery: [
+ "attributeID", "value",
+ /* covering: */ "contactID"],
+ }
+ },
+
+ /**
+ * Identities correspond to specific e-mail addresses, IRC nicks, etc.
+ */
+ identities: {
+ columns: [
+ "id INTEGER PRIMARY KEY",
+ "contactID INTEGER NOT NULL REFERENCES contacts(id)",
+ "kind TEXT NOT NULL", // ex: email, irc, etc.
+ "value TEXT NOT NULL", // ex: e-mail address, irc nick/handle, etc.
+ "description NOT NULL", // what makes this identity different from the
+ // others? (ex: home, work, etc.)
+ "relay INTEGER NOT NULL", // is the identity just a relay mechanism?
+ // (ex: mailing list, twitter 'bouncer', IRC gateway, etc.)
+ ],
+
+ indices: {
+ contactQuery: ["contactID"],
+ valueQuery: ["kind", "value"]
+ }
+ },
+
+ //identityAttributes: {
+ //},
+
+ },
+ },
+
+
+ /* ******************* LOGIC ******************* */
+ /**
+ * Our synchronous connection, primarily intended for read-only use, so as to
+ * avoid stepping on the toes of our asynchronous connection that will do
+ * most/all of our updating.
+ */
+ syncConnection: null,
+ /**
+ * Our connection reused for asynchronous usage, intended for database write
+ * purposes.
+ */
+ asyncConnection: null,
+
+ /**
+ * Initialize logging, create the database if it doesn't exist, "upgrade" it
+ * if it does and it's not up-to-date, fill our authoritative folder uri/id
+ * mapping.
+ */
+ _init: function gloda_ds_init(aNsJSON, aNounIDToDef) {
+ this._log = Log4Moz.Service.getLogger("gloda.datastore");
+ this._log.debug("Beginning datastore initialization.");
+
+ this._json = aNsJSON;
+ this._nounIDToDef = aNounIDToDef;
+
+ // Get the path to our global database
+ var dirService = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties);
+ var dbFile = dirService.get("ProfD", Ci.nsIFile);
+ dbFile.append("global-messages-db.sqlite");
+
+ // Get the storage (sqlite) service
+ var dbService = Cc["@mozilla.org/storage/service;1"].
+ getService(Ci.mozIStorageService);
+
+ var dbConnection;
+
+ // Create the file if it does not exist
+ if (!dbFile.exists()) {
+ this._log.debug("Creating database because it does't exist.");
+ dbConnection = this._createDB(dbService, dbFile);
+ }
+ // It does exist, but we (someday) might need to upgrade the schema
+ else {
+ // (Exceptions may be thrown if the database is corrupt)
+ { // try {
+ dbConnection = dbService.openUnsharedDatabase(dbFile);
+
+ if (dbConnection.schemaVersion != this._schemaVersion) {
+ this._log.debug("Need to migrate database. (DB version: " +
+ dbConnection.schemaVersion + " desired version: " +
+ this._schemaVersion);
+ dbConnection = this._migrate(dbService, dbFile,
+ dbConnection,
+ dbConnection.schemaVersion,
+ this._schemaVersion);
+ this._log.debug("Migration completed.");
+ }
+ }
+ // Handle corrupt databases, other oddities
+ // ... in the future. for now, let us die
+ }
+
+ this.syncConnection = dbConnection;
+ this.asyncConnection = dbConnection;
+
+ this._log.debug("Initializing folder mappings.");
+ this._getAllFolderMappings();
+ // we need to figure out the next id's for all of the tables where we
+ // manage that.
+ this._log.debug("Populating managed id counters.");
+ this._populateAttributeDefManagedId();
+ this._populateConversationManagedId();
+ this._populateMessageManagedId();
+ this._populateContactManagedId();
+ this._populateIdentityManagedId();
+
+ this._log.debug("Completed datastore initialization.");
+ },
+
+ /**
+ * Initiate database shutdown; because this might requiring waiting for
+ * outstanding synchronous events to drain, we allow the caller to pass in
+ * a callback to invoke if we are unable to complete shutdown within this
+ * call.
+ * @return true if we were able to shutdown fully, false if we were not. The
+ * callback, if provided, will be notified if we return false. It will
+ * not be called if we return true.
+ */
+ shutdown: function gloda_ds_shutdown(aCallback, aCallbackThis) {
+ // clear out any transaction
+ while (this._transactionDepth) {
+ this._log.info("Closing pending transaction out for shutdown.");
+ // just schedule this function to be run again once the transaction has
+ // been closed out.
+ this._commitTransaction();
+ }
+
+ let datastore = this;
+
+ function finish_cleanup() {
+ datastore._cleanupAsyncStatements();
+ datastore._cleanupSyncStatements();
+ datastore._log.info("Closing db connection");
+ datastore.asyncConnection.close();
+ datastore.asyncConnection = null;
+ datastore.syncConnection = null;
+
+ if (aCallback) {
+ aCallback.call(aCallbackThis);
+ }
+ }
+
+ if (this._pendingAsyncStatements) {
+ this._pendingAsyncCompletedListener = finish_cleanup;
+ return false;
+ }
+ else {
+ this._log.debug("There are no pending async statements, finishing now.");
+ aCallback = null;
+ finish_cleanup();
+ return true;
+ }
+ },
+
+ /**
+ * Create our database; basically a wrapper around _createSchema.
+ */
+ _createDB: function gloda_ds_createDB(aDBService, aDBFile) {
+ var dbConnection = aDBService.openUnsharedDatabase(aDBFile);
+
+ dbConnection.beginTransaction();
+ try {
+ this._createSchema(dbConnection);
+ dbConnection.commitTransaction();
+ }
+ catch(ex) {
+ dbConnection.rollbackTransaction();
+ throw ex;
+ }
+
+ return dbConnection;
+ },
+
+ _createTableSchema: function gloda_ds_createTableSchema(aDBConnection,
+ aTableName) {
+ let table = this._schema.tables[aTableName];
+
+ // - Create the table
+ aDBConnection.createTable(aTableName, table.columns.join(", "));
+
+ // - Create the fulltext table if applicable
+ if ("fulltextColumns" in table) {
+ let createFulltextSQL = "CREATE VIRTUAL TABLE " + aTableName + "Text" +
+ " USING fts3(tokenize porter, " + table.fulltextColumns.join(", ") +
+ ")";
+ this._log.info("Create fulltext: " + createFulltextSQL);
+ aDBConnection.executeSimpleSQL(createFulltextSQL);
+ }
+
+ // - Create its indices
+ for (let indexName in table.indices) {
+ let indexColumns = table.indices[indexName];
+
+ aDBConnection.executeSimpleSQL(
+ "CREATE INDEX " + indexName + " ON " + aTableName +
+ "(" + indexColumns.join(", ") + ")");
+ }
+ },
+
+ /**
+ * Create our database schema assuming a newly created database. This
+ * comes down to creating normal tables, their full-text variants (if
+ * applicable), and their indices.
+ */
+ _createSchema: function gloda_ds_createSchema(aDBConnection) {
+ // -- For each table...
+ for (let tableName in this._schema.tables) {
+ this._createTableSchema(aDBConnection, tableName);
+ }
+
+ aDBConnection.schemaVersion = this._schemaVersion;
+ },
+
+ /**
+ * Our table definition used here is slightly different from that used
+ * internally, because we are potentially creating a sort of crappy ORM and
+ * we don't want to have to parse the column names out.
+ */
+ createTableIfNotExists: function gloda_ds_createTableIfNotExists(aTableDef) {
+ aTableDef._realName = "ext_" + aTableDef.name;
+
+ // first, check if the table exists
+ if (!this.asyncConnection.tableExists(aTableDef._realName)) {
+ try {
+ this.asyncConnection.createTable(aTableDef._realName,
+ [coldef.join(" ") for each
+ ([i, coldef] in Iterator(aTableDef.columns))].join(", "));
+ }
+ catch (ex) {
+ this._log.error("Problem creating table " + aTableDef.name + " " +
+ "because: " + ex + " at " + ex.fileName + ":" + ex.lineNumber);
+ return null;
+ }
+
+ for (let indexName in aTableDef.indices) {
+ let indexColumns = aTableDef.indices[indexName];
+
+ try {
+ let indexSql = "CREATE INDEX " + indexName + " ON " +
+ aTableDef._realName + " (" + indexColumns.join(", ") + ")";
+ this.asyncConnection.executeSimpleSQL(indexSql);
+ }
+ catch (ex) {
+ this._log.error("Problem creating index " + indexName + " for " +
+ "table " + aTableDef.name + " because " + ex + " at " +
+ ex.fileName + ":" + ex.lineNumber);
+ }
+ }
+ }
+
+ return new GlodaDatabind(aTableDef, this);
+ },
+
+ _migrate: function gloda_ds_migrate(aDBService, aDBFile, aDBConnection,
+ aCurVersion, aNewVersion) {
+ // we purged our way up to version 8, so we can/must purge prior to 8.
+ if (aCurVersion < 8) {
+ aDBConnection.close();
+ aDBFile.remove(false);
+ this._log.warn("Global database has been purged due to schema change.");
+ return this._createDB(aDBService, aDBFile);
+ }
+ // version 9 just adds the contactAttributes table
+ if (aCurVersion < 9) {
+ this._createTableSchema(aDBConnection, "contactAttributes");
+ }
+ // version 10:
+ // we have so many changes here, not to mention semantic changes, that
+ // purging is the right answer.
+ // - adds dirtyStatus, name to folderLocations
+ // - removes messageAttribFetch index from messageAttributes
+ // - removes conversationAttribFetch index from messageAttributes
+ // - removes contactAttribFetch index from contactAttributes
+ // - adds jsonAttributes column to messages table
+ // - adds jsonAttributes column to contacts table
+ if (aCurVersion < 10) {
+ aDBConnection.close();
+ aDBFile.remove(false);
+ this._log.warn("Global database has been purged due to schema change.");
+ return this._createDB(aDBService, aDBFile);
+ }
+
+ aDBConnection.schemaVersion = aNewVersion;
+
+ return aDBConnection;
+ },
+
+ _outstandingAsyncStatements: [],
+
+ _createAsyncStatement: function gloda_ds_createAsyncStatement(aSQLString,
+ aWillFinalize) {
+ let statement = null;
+ try {
+ statement = this.asyncConnection.createStatement(aSQLString);
+ }
+ catch(ex) {
+ throw("error creating async statement " + aSQLString + " - " +
+ this.asyncConnection.lastError + ": " +
+ this.asyncConnection.lastErrorString + " - " + ex);
+ }
+
+ if (!aWillFinalize)
+ this._outstandingAsyncStatements.push(statement);
+
+ return statement;
+ },
+
+ _cleanupAsyncStatements: function gloda_ds_cleanupAsyncStatements() {
+ [stmt.finalize() for each
+ ([i, stmt] in Iterator(this._outstandingAsyncStatements))];
+ },
+
+ _outstandingSyncStatements: [],
+
+ _createSyncStatement: function gloda_ds_createSyncStatement(aSQLString,
+ aWillFinalize) {
+ let statement = null;
+ try {
+ statement = this.syncConnection.createStatement(aSQLString);
+ }
+ catch(ex) {
+ throw("error creating sync statement " + aSQLString + " - " +
+ this.syncConnection.lastError + ": " +
+ this.syncConnection.lastErrorString + " - " + ex);
+ }
+
+ if (!aWillFinalize)
+ this._outstandingSyncStatements.push(statement);
+
+ return statement;
+ },
+
+ _cleanupSyncStatements: function gloda_ds_cleanupSyncStatements() {
+ [stmt.finalize() for each
+ ([i, stmt] in Iterator(this._outstandingSyncStatements))];
+ },
+
+ /**
+ * Perform a synchronous executeStep on the statement, handling any
+ * SQLITE_BUSY fallout that could conceivably happen from a collision on our
+ * read with the async writes.
+ * Basically we keep trying until we succeed or run out of tries.
+ * We believe this to be a reasonable course of action because we don't
+ * expect this to happen much.
+ */
+ _syncStep: function gloda_ds_syncStep(aStatement) {
+ let tries = 0;
+ while (tries < 32000) {
+ try {
+ return aStatement.executeStep();
+ }
+ // SQLITE_BUSY becomes NS_ERROR_FAILURE
+ catch (e if e.result == 0x80004005) {
+ tries++;
+ // we really need to delay here, somehow. unfortunately, we can't
+ // allow event processing to happen, and most of the things we could
+ // do to delay ourselves result in event processing happening. (Use
+ // of a timer, a synchronous dispatch, etc.)
+ // in theory, nsIThreadEventFilter could allow us to stop other events
+ // that aren't our timer from happening, but it seems slightly
+ // dangerous and 'notxpcom' suggests it ain't happening anyways...
+ // so, let's just be dumb and hope that the underlying file I/O going
+ // on makes us more likely to yield to the other thread so it can
+ // finish what it is doing...
+ }
+ }
+ this._log.error("Synchronous step gave up after " + tries + " tries.");
+ },
+
+ /**
+ * Helper to bind based on the actual type of the javascript value. Note
+ * that we always use int64 because under the hood sqlite just promotes the
+ * normal 'int' call to 'int64' anyways.
+ */
+ _bindVariant: function gloda_ds_bindBlob(aStatement, aIndex, aVariant) {
+ if (aVariant == null) // catch both null and undefined
+ aStatement.bindNullParameter(aIndex);
+ else if (typeof aVariant == "string")
+ aStatement.bindStringParameter(aIndex, aVariant);
+ else if (typeof aVariant == "number") {
+ // we differentiate for storage representation reasons only.
+ if (Math.floor(aVariant) === aVariant)
+ aStatement.bindInt64Parameter(aIndex, aVariant);
+ else
+ aStatement.bindDoubleParameter(aIndex, aVariant);
+ }
+ else
+ throw("Attempt to bind variant with unsupported type: " +
+ (typeof aVariant));
+ },
+
+ _getVariant: function gloda_ds_getBlob(aRow, aIndex) {
+ let typeOfIndex = aRow.getTypeOfIndex(aIndex);
+ if (typeOfIndex == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
+ return null;
+ // XPConnect would just end up going through an intermediary double stage
+ // for the int64 case anyways...
+ else if (typeOfIndex == Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER ||
+ typeOfIndex == Ci.mozIStorageValueArray.VALUE_TYPE_DOUBLE)
+ return aRow.getDouble(aIndex);
+ else // typeOfIndex == Ci.mozIStorageValueArray.VALUE_TYPE_TEXT
+ return aRow.getString(aIndex);
+ },
+
+ /** Simple nested transaction support as a performance optimization. */
+ _transactionDepth: 0,
+ _transactionGood: false,
+
+ get _beginTransactionStatement() {
+ let statement = this._createAsyncStatement("BEGIN TRANSACTION");
+ this.__defineGetter__("_beginTransactionStatement", function() statement);
+ return this._beginTransactionStatement;
+ },
+
+ get _commitTransactionStatement() {
+ let statement = this._createAsyncStatement("COMMIT");
+ this.__defineGetter__("_commitTransactionStatement", function() statement);
+ return this._commitTransactionStatement;
+ },
+
+ get _rollbackTransactionStatement() {
+ let statement = this._createAsyncStatement("ROLLBACK");
+ this.__defineGetter__("_rollbackTransactionStatement", function() statement);
+ return this._rollbackTransactionStatement;
+ },
+
+ _pendingPostCommitCallbacks: null,
+ /**
+ * Register a callback to be invoked when the current transaction's commit
+ * completes.
+ */
+ runPostCommit: function gloda_ds_runPostCommit(aCallback) {
+ this._pendingPostCommitCallbacks.push(aCallback);
+ },
+
+ /**
+ * Begin a potentially nested transaction; only the outermost transaction gets
+ * to be an actual transaction, and the failure of any nested transaction
+ * results in a rollback of the entire outer transaction. If you really
+ * need an atomic transaction
+ */
+ _beginTransaction: function gloda_ds_beginTransaction() {
+ if (this._transactionDepth == 0) {
+ this._pendingPostCommitCallbacks = [];
+ this._beginTransactionStatement.executeAsync(this.trackAsync());
+ this._transactionGood = true;
+ }
+ this._transactionDepth++;
+ },
+ /**
+ * Commit a potentially nested transaction; if we are the outer-most
+ * transaction and no sub-transaction issues a rollback
+ * (via _rollbackTransaction) then we commit, otherwise we rollback.
+ */
+ _commitTransaction: function gloda_ds_commitTransaction() {
+ this._transactionDepth--;
+ if (this._transactionDepth == 0) {
+ try {
+ if (this._transactionGood)
+ this._commitTransactionStatement.executeAsync(
+ new PostCommitHandler(this._pendingPostCommitCallbacks));
+ else
+ this._rollbackTransactionStatement.executeAsync(this.trackAsync());
+ }
+ catch (ex) {
+ this._log.error("Commit problem: " + ex);
+ }
+ this._pendingPostCommitCallbacks = [];
+ }
+ },
+ /**
+ * Abort the commit of the potentially nested transaction. If we are not the
+ * outermost transaction, we set a flag that tells the outermost transaction
+ * that it must roll back.
+ */
+ _rollbackTransaction: function gloda_ds_rollbackTransaction() {
+ this._transactionDepth--;
+ this._transactionGood = false;
+ if (this._transactionDepth == 0) {
+ try {
+ this._rollbackTransactionStatement.executeAsync(this.trackAsync());
+ }
+ catch (ex) {
+ this._log.error("Rollback problem: " + ex);
+ }
+ }
+ },
+
+ _pendingAsyncStatements: 0,
+ /**
+ * The function to call, if any, when we hit 0 pending async statements.
+ */
+ _pendingAsyncCompletedListener: null,
+ _asyncCompleted: function () {
+ if (--this._pendingAsyncStatements == 0) {
+ if (this._pendingAsyncCompletedListener !== null) {
+ this._pendingAsyncCompletedListener();
+ this._pendingAsyncCompletedListener = null;
+ }
+ }
+ },
+ _asyncTrackerListener: {
+ handleResult: function () {},
+ handleError: function() {},
+ handleCompletion: function () {
+ // the helper method exists because the other classes need to call it too
+ GlodaDatastore._asyncCompleted();
+ }
+ },
+ /**
+ * Increments _pendingAsyncStatements and returns a listener that will
+ * decrement the value when the statement completes.
+ */
+ trackAsync: function() {
+ this._pendingAsyncStatements++;
+ return this._asyncTrackerListener;
+ },
+
+ /* ********** Attribute Definitions ********** */
+ /** Maps (attribute def) compound names to the GlodaAttributeDBDef objects. */
+ _attributeDBDefs: {},
+ /** Map attribute ID to the definition and parameter value that produce it. */
+ _attributeIDToDBDefAndParam: {},
+ /**
+ * We maintain the attributeDefinitions next id counter mainly because we can.
+ * Since we mediate the access, there's no real risk to doing so, and it
+ * allows us to keep the writes on the async connection without having to
+ * wait for a completion notification.
+ */
+ _nextAttributeId: 1,
+
+ _populateAttributeDefManagedId: function () {
+ let stmt = this._createSyncStatement(
+ "SELECT MAX(id) FROM attributeDefinitions", true);
+ if (stmt.executeStep()) { // no chance of this SQLITE_BUSY on this call
+ this._nextAttributeId = stmt.getInt64(0) + 1;
+ }
+ stmt.finalize();
+ },
+
+ get _insertAttributeDefStatement() {
+ let statement = this._createAsyncStatement(
+ "INSERT INTO attributeDefinitions (id, attributeType, extensionName, \
+ name, parameter) \
+ VALUES (?1, ?2, ?3, ?4, ?5)");
+ this.__defineGetter__("_insertAttributeDefStatement", function() statement);
+ return this._insertAttributeDefStatement;
+ },
+
+ /**
+ * Create an attribute definition and return the row ID. Special/atypical
+ * in that it doesn't directly return a GlodaAttributeDBDef; we leave that up
+ * to the caller since they know much more than actually needs to go in the
+ * database.
+ *
+ * @return The attribute id allocated to this attribute.
+ */
+ _createAttributeDef: function gloda_ds_createAttributeDef(aAttrType,
+ aExtensionName, aAttrName, aParameter) {
+ let attributeId = this._nextAttributeId++;
+
+ let iads = this._insertAttributeDefStatement;
+ iads.bindInt64Parameter(0, attributeId);
+ iads.bindInt64Parameter(1, aAttrType);
+ iads.bindStringParameter(2, aExtensionName);
+ iads.bindStringParameter(3, aAttrName);
+ this._bindVariant(iads, 4, aParameter);
+
+ iads.executeAsync(this.trackAsync());
+
+ return attributeId;
+ },
+
+ /**
+ * Sync-ly look-up all the attribute definitions, populating our authoritative
+ * _attributeDBDefss and _attributeIDToDBDefAndParam maps. (In other words,
+ * once this method is called, those maps should always be in sync with the
+ * underlying database.)
+ */
+ getAllAttributes: function gloda_ds_getAllAttributes() {
+ let stmt = this._createSyncStatement(
+ "SELECT id, attributeType, extensionName, name, parameter \
+ FROM attributeDefinitions", true);
+
+ // map compound name to the attribute
+ let attribs = {};
+ // map the attribute id to [attribute, parameter] where parameter is null
+ // in cases where parameter is unused.
+ let idToAttribAndParam = {}
+
+ this._log.info("loading all attribute defs");
+
+ while (stmt.executeStep()) { // no chance of this SQLITE_BUSY on this call
+ let rowId = stmt.getInt64(0);
+ let rowAttributeType = stmt.getInt64(1);
+ let rowExtensionName = stmt.getString(2);
+ let rowName = stmt.getString(3);
+ let rowParameter = this._getVariant(stmt, 4);
+
+ let compoundName = rowExtensionName + ":" + rowName;
+
+ let attrib;
+ if (compoundName in attribs) {
+ attrib = attribs[compoundName];
+ } else {
+ attrib = new GlodaAttributeDBDef(this, /* aID */ null,
+ compoundName, rowAttributeType, rowExtensionName, rowName);
+ attribs[compoundName] = attrib;
+ }
+ // if the parameter is null, the id goes on the attribute def, otherwise
+ // it is a parameter binding and goes in the binding map.
+ if (rowParameter == null) {
+ this._log.debug(compoundName + " primary: " + rowId);
+ attrib._id = rowId;
+ idToAttribAndParam[rowId] = [attrib, null];
+ } else {
+ this._log.debug(compoundName + " binding: " + rowParameter +
+ " = " + rowId);
+ attrib._parameterBindings[rowParameter] = rowId;
+ idToAttribAndParam[rowId] = [attrib, rowParameter];
+ }
+ }
+ stmt.finalize();
+
+ this._log.info("done loading all attribute defs");
+
+ this._attributeDBDefs = attribs;
+ this._attributeIDToDBDefAndParam = idToAttribAndParam;
+ },
+
+ /**
+ * Helper method for GlodaAttributeDBDef to tell us when their bindParameter
+ * method is called and they have created a new binding (using
+ * GlodaDatastore._createAttributeDef). In theory, that method could take
+ * an additional argument and obviate the need for this method.
+ */
+ reportBinding: function gloda_ds_reportBinding(aID, aAttrDef, aParamValue) {
+ this._attributeIDToDBDefAndParam[aID] = [aAttrDef, aParamValue];
+ },
+
+ /* ********** Folders ********** */
+ /** next folder (row) id to issue, populated by _getAllFolderMappings. */
+ _nextFolderId: 1,
+
+ get _insertFolderLocationStatement() {
+ let statement = this._createAsyncStatement(
+ "INSERT INTO folderLocations (id, folderURI, dirtyStatus, name) VALUES \
+ (?1, ?2, ?3, ?4)");
+ this.__defineGetter__("_insertFolderLocationStatement",
+ function() statement);
+ return this._insertFolderLocationStatement;
+ },
+
+ /**
+ * Authoritative map from folder URI to folder ID. (Authoritative in the
+ * sense that this map exactly represents the state of the underlying
+ * database. If it does not, it's a bug in updating the database.)
+ */
+ _folderByURI: {},
+ /** Authoritative map from folder ID to folder URI */
+ _folderByID: {},
+
+ /** Intialize our _folderByURI/_folderByID mappings, called by _init(). */
+ _getAllFolderMappings: function gloda_ds_getAllFolderMappings() {
+ let stmt = this._createSyncStatement(
+ "SELECT id, folderURI, dirtyStatus, name FROM folderLocations", true);
+
+ while (stmt.executeStep()) { // no chance of this SQLITE_BUSY on this call
+ let folderID = stmt.getInt64(0);
+ let folderURI = stmt.getString(1);
+ let dirtyStatus = stmt.getInt32(2);
+ let folderName = stmt.getString(3);
+
+ let folder = new GlodaFolder(this, folderID, folderURI, dirtyStatus,
+ folderName);
+
+ this._folderByURI[folderURI] = folder;
+ this._folderByID[folderID] = folder;
+
+ if (folderID >= this._nextFolderId)
+ this._nextFolderId = folderID + 1;
+ }
+ stmt.finalize();
+ },
+
+ _folderKnown: function gloda_ds_folderKnown(aFolder) {
+ let folderURI = aFolder.URI;
+ return folderURI in this._folderByURI;
+ },
+
+ /**
+ * Map a folder URI to a folder ID, creating the mapping if it does not yet
+ * exist.
+ */
+ _mapFolder: function gloda_ds_mapFolderURI(aFolder) {
+ let folderURI = aFolder.URI;
+ if (folderURI in this._folderByURI) {
+ return this._folderByURI[folderURI];
+ }
+
+ let folderID = this._nextFolderId++;
+
+ let folder = new GlodaFolder(this, folderID, folderURI,
+ GlodaFolder.prototype.kFolderFilthy, aFolder.prettiestName);
+
+ this._insertFolderLocationStatement.bindInt64Parameter(0, folder.id)
+ this._insertFolderLocationStatement.bindStringParameter(1, folder.uri);
+ this._insertFolderLocationStatement.bindInt64Parameter(2,
+ folder.dirtyStatus);
+ this._insertFolderLocationStatement.bindStringParameter(3, folder.name);
+ this._insertFolderLocationStatement.executeAsync(this.trackAsync());
+
+ this._folderByURI[folderURI] = folder;
+ this._folderByID[folderID] = folder;
+ this._log.debug("!! mapped " + folder.id + " from " + folderURI);
+ return folder;
+ },
+
+ _mapFolderID: function gloda_ds_mapFolderID(aFolderID) {
+ if (aFolderID === null)
+ return null;
+ if (aFolderID in this._folderByID)
+ return this._folderByID[aFolderID];
+ throw "Got impossible folder ID: " + aFolderID;
+ },
+
+ get _updateFolderDirtyStatusStatement() {
+ let statement = this._createAsyncStatement(
+ "UPDATE folderLocations SET dirtyStatus = ?1 \
+ WHERE id = ?2");
+ this.__defineGetter__("_updateFolderDirtyStatusStatement",
+ function() statement);
+ return this._updateFolderDirtyStatusStatement;
+ },
+
+ updateFolderDirtyStatus: function gloda_ds_updateFolderDirtyStatus(aFolder) {
+ let ufds = this._updateFolderDirtyStatusStatement;
+ ufds.bindInt64Parameter(1, aFolder.id);
+ ufds.bindInt64Parameter(0, aFolder.dirtyStatus);
+ ufds.executeAsync(this.trackAsync());
+ },
+
+ get _updateFolderLocationStatement() {
+ let statement = this._createAsyncStatement(
+ "UPDATE folderLocations SET folderURI = ?1 \
+ WHERE id = ?2");
+ this.__defineGetter__("_updateFolderLocationStatement",
+ function() statement);
+ return this._updateFolderLocationStatement;
+ },
+
+ /**
+ * Non-recursive asynchronous folder renaming based on the URI.
+ *
+ * @TODO provide a mechanism for recursive folder renames or have a higher
+ * layer deal with it and remove this note.
+ */
+ renameFolder: function gloda_ds_renameFolder(aOldFolder, aNewURI) {
+ let folder = this._mapFolder(aOldFolder); // ensure the folder is mapped
+ let oldURI = folder.uri;
+ this._folderByURI[aNewURI] = folder;
+ folder._uri = aNewURI;
+ this._log.info("renaming folder URI " + oldURI + " to " + aNewURI);
+ this._updateFolderLocationStatement.bindStringParameter(1, folder.id);
+ this._updateFolderLocationStatement.bindStringParameter(0, aNewURI);
+ this._updateFolderLocationStatement.executeAsync(this.trackAsync());
+
+ delete this._folderByURI[oldURI];
+ },
+
+ get _deleteFolderByIDStatement() {
+ let statement = this._createAsyncStatement(
+ "DELETE FROM folderLocations WHERE id = ?1");
+ this.__defineGetter__("_deleteFolderByIDStatement",
+ function() statement);
+ return this._deleteFolderByIDStatement;
+ },
+
+ deleteFolderByID: function gloda_ds_deleteFolder(aFolderID) {
+ let dfbis = this._deleteFolderByIDStatement;
+ dfbis.bindInt64Parameter(0, aFolderID);
+ dfbis.executeAsync(this.trackAsync());
+ },
+
+ /* ********** Conversation ********** */
+ /** The next conversation id to allocate. Initialize at startup. */
+ _nextConversationId: 1,
+
+ _populateConversationManagedId: function () {
+ let stmt = this._createSyncStatement(
+ "SELECT MAX(id) FROM conversations", true);
+ if (stmt.executeStep()) { // no chance of this SQLITE_BUSY on this call
+ this._nextConversationId = stmt.getInt64(0) + 1;
+ }
+ stmt.finalize();
+ },
+
+ get _insertConversationStatement() {
+ let statement = this._createAsyncStatement(
+ "INSERT INTO conversations (id, subject, oldestMessageDate, \
+ newestMessageDate) \
+ VALUES (?1, ?2, ?3, ?4)");
+ this.__defineGetter__("_insertConversationStatement", function() statement);
+ return this._insertConversationStatement;
+ },
+
+ get _insertConversationTextStatement() {
+ let statement = this._createAsyncStatement(
+ "INSERT INTO conversationsText (docid, subject) \
+ VALUES (?1, ?2)");
+ this.__defineGetter__("_insertConversationTextStatement",
+ function() statement);
+ return this._insertConversationTextStatement;
+ },
+
+ /**
+ * Asynchronously create a conversation.
+ */
+ createConversation: function gloda_ds_createConversation(aSubject,
+ aOldestMessageDate, aNewestMessageDate) {
+
+ // create the data row
+ let conversationID = this._nextConversationId++;
+ let ics = this._insertConversationStatement;
+ ics.bindInt64Parameter(0, conversationID);
+ ics.bindStringParameter(1, aSubject);
+ if (aOldestMessageDate == null)
+ ics.bindNullParameter(2);
+ else
+ ics.bindInt64Parameter(2, aOldestMessageDate);
+ if (aNewestMessageDate == null)
+ ics.bindNullParameter(3);
+ else
+ ics.bindInt64Parameter(3, aNewestMessageDate);
+ ics.executeAsync(this.trackAsync());
+
+ // create the fulltext row, using the same rowid/docid
+ let icts = this._insertConversationTextStatement;
+ icts.bindInt64Parameter(0, conversationID);
+ icts.bindStringParameter(1, aSubject);
+ icts.executeAsync(this.trackAsync());
+
+ // create it
+ let conversation = new GlodaConversation(this, conversationID,
+ aSubject, aOldestMessageDate,
+ aNewestMessageDate);
+ // it's new! let the collection manager know about it.
+ GlodaCollectionManager.itemsAdded(conversation.NOUN_ID, [conversation]);
+ // return it
+ return conversation;
+ },
+
+ get _deleteConversationByIDStatement() {
+ let statement = this._createAsyncStatement(
+ "DELETE FROM conversations WHERE id = ?1");
+ this.__defineGetter__("_deleteConversationByIDStatement",
+ function() statement);
+ return this._deleteConversationByIDStatement;
+ },
+
+ /**
+ * Asynchronously delete a conversation given its ID.
+ */
+ deleteConversationByID: function gloda_ds_deleteConversationByID(
+ aConversationID) {
+ let dcbids = this._deleteConversationByIDStatement;
+ dcbids.bindInt64Parameter(0, aConversationID);
+ dcbids.executeAsync(this.trackAsync());
+
+ // TODO: collection manager implications
+ //GlodaCollectionManager.removeByID()
+ },
+
+ get _selectConversationByIDStatement() {
+ let statement = this._createSyncStatement(
+ "SELECT id, subject, oldestMessageDate, newestMessageDate \
+ FROM conversations WHERE id = ?1");
+ this.__defineGetter__("_selectConversationByIDStatement",
+ function() statement);
+ return this._selectConversationByIDStatement;
+ },
+
+ _conversationFromRow: function gloda_ds_conversationFromRow(aStmt) {
+ let oldestMessageDate, newestMessageDate;
+ if (aStmt.getTypeOfIndex(2) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
+ oldestMessageDate = null;
+ else
+ oldestMessageDate = aStmt.getInt64(2);
+ if (aStmt.getTypeOfIndex(3) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
+ newestMessageDate = null;
+ else
+ newestMessageDate = aStmt.getInt64(3);
+ return new GlodaConversation(this, aStmt.getInt64(0),
+ aStmt.getString(1), oldestMessageDate, newestMessageDate);
+ },
+
+ /**
+ * Synchronously look up a conversation given its ID.
+ */
+ getConversationByID: function gloda_ds_getConversationByID(aConversationID) {
+ let conversation = GlodaCollectionManager.cacheLookupOne(
+ GlodaConversation.prototype.NOUN_ID, aConversationID);
+
+ if (conversation === null) {
+ let scbids = this._selectConversationByIDStatement;
+
+ scbids.bindInt64Parameter(0, aConversationID);
+ if (this._syncStep(scbids)) {
+ conversation = this._conversationFromRow(scbids);
+ GlodaCollectionManager.itemLoaded(conversation);
+ }
+ scbids.reset();
+ }
+
+ return conversation;
+ },
+
+ /* ********** Message ********** */
+ /**
+ * Next message id, managed because of our use of asynchronous inserts.
+ * Initialized by _populateMessageManagedId called by _init.
+ */
+ _nextMessageId: 1,
+
+ _populateMessageManagedId: function () {
+ let stmt = this._createSyncStatement(
+ "SELECT MAX(id) FROM messages", true);
+ if (stmt.executeStep()) { // no chance of this SQLITE_BUSY on this call
+ this._nextMessageId = stmt.getInt64(0) + 1;
+ }
+ stmt.finalize();
+ },
+
+ get _insertMessageStatement() {
+ let statement = this._createAsyncStatement(
+ "INSERT INTO messages (id, folderID, messageKey, conversationID, date, \
+ headerMessageID, jsonAttributes) \
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)");
+ this.__defineGetter__("_insertMessageStatement", function() statement);
+ return this._insertMessageStatement;
+ },
+
+ get _insertMessageTextStatement() {
+ let statement = this._createAsyncStatement(
+ "INSERT INTO messagesText (docid, subject, body, attachmentNames) \
+ VALUES (?1, ?2, ?3, ?4)");
+ this.__defineGetter__("_insertMessageTextStatement", function() statement);
+ return this._insertMessageTextStatement;
+ },
+
+ /**
+ * Create a GlodaMessage with the given properties. Because this is only half
+ * of the process of creating a message (the attributes still need to be
+ * completed), it's on the caller's head to call GlodaCollectionManager's
+ * itemAdded method once the message is fully created.
+ *
+ * This method uses the async connection, any downstream logic that depends on
+ * this message actually existing in the database must be done using an
+ * async query.
+ */
+ createMessage: function gloda_ds_createMessage(aFolder, aMessageKey,
+ aConversationID, aDatePRTime, aHeaderMessageID) {
+ let folderID;
+ if (aFolder != null) {
+ folderID = this._mapFolder(aFolder).id;
+ }
+ else {
+ folderID = null;
+ }
+
+ let messageID = this._nextMessageId++;
+
+ let message = new GlodaMessage(this, messageID, folderID,
+ aMessageKey, aConversationID, null,
+ aDatePRTime ? new Date(aDatePRTime / 1000) : null,
+ aHeaderMessageID);
+
+ this._log.debug("CreateMessage: " + folderID + ", " + aMessageKey + ", " +
+ aConversationID + ", " + aDatePRTime + ", " +
+ aHeaderMessageID);
+
+ // We would love to notify the collection manager about the message at this
+ // point (at least if it's not a ghost), but we can't yet. We need to wait
+ // until the attributes have been indexed, which means it's out of our
+ // hands. (Gloda.processMessage does it.)
+
+ return message;
+ },
+
+ insertMessage: function gloda_ds_insertMessage(aMessage) {
+ let ims = this._insertMessageStatement;
+ ims.bindInt64Parameter(0, aMessage.id);
+ if (aMessage.folderID == null)
+ ims.bindNullParameter(1);
+ else
+ ims.bindInt64Parameter(1, aMessage.folderID);
+ if (aMessage.messageKey == null)
+ ims.bindNullParameter(2);
+ else
+ ims.bindInt64Parameter(2, aMessage.messageKey);
+ ims.bindInt64Parameter(3, aMessage.conversationID);
+ if (aMessage.date == null)
+ ims.bindNullParameter(4);
+ else
+ ims.bindInt64Parameter(4, aMessage.date * 1000);
+ ims.bindStringParameter(5, aMessage.headerMessageID);
+ if (aMessage._jsonText)
+ ims.bindStringParameter(6, aMessage._jsonText);
+ else
+ ims.bindNullParameter(6);
+
+ try {
+ ims.executeAsync(this.trackAsync());
+ }
+ catch(ex) {
+ throw("error executing statement... " +
+ this.asyncConnection.lastError + ": " +
+ this.asyncConnection.lastErrorString + " - " + ex);
+ }
+
+ // we only create the full-text row if the body is non-null.
+ // so, even though body might be null, we still want to create the
+ // full-text search row
+ if (aMessage._bodyLines) {
+ let bodyText;
+ if (aMessage._content && aMessage._content.hasContent())
+ bodyText = aMessage._content.getContentString(true);
+ else
+ bodyText = aMessage._bodyLines.join("\n");
+
+ let imts = this._insertMessageTextStatement;
+ imts.bindInt64Parameter(0, aMessage.id);
+ imts.bindStringParameter(1, aMessage._subject);
+ imts.bindStringParameter(2, bodyText);
+ if (aMessage._attachmentNames === null)
+ imts.bindNullParameter(3);
+ else
+ imts.bindStringParameter(3, aMessage._attachmentNames);
+
+ try {
+ imts.executeAsync(this.trackAsync());
+ }
+ catch(ex) {
+ throw("error executing fulltext statement... " +
+ this.asyncConnection.lastError + ": " +
+ this.asyncConnection.lastErrorString + " - " + ex);
+ }
+ }
+ },
+
+ get _updateMessageStatement() {
+ let statement = this._createAsyncStatement(
+ "UPDATE messages SET folderID = ?1, \
+ messageKey = ?2, \
+ conversationID = ?3, \
+ date = ?4, \
+ headerMessageID = ?5, \
+ jsonAttributes = ?6 \
+ WHERE id = ?7");
+ this.__defineGetter__("_updateMessageStatement", function() statement);
+ return this._updateMessageStatement;
+ },
+
+ /**
+ * Update the database row associated with the message. If aBody is supplied,
+ * the associated full-text row is created; it is assumed that it did not
+ * previously exist.
+ */
+ updateMessage: function gloda_ds_updateMessage(aMessage) {
+ let ums = this._updateMessageStatement;
+ ums.bindInt64Parameter(6, aMessage.id);
+ if (aMessage.folderID === null)
+ ums.bindNullParameter(0);
+ else
+ ums.bindInt64Parameter(0, aMessage.folderID);
+ if (aMessage.messageKey === null)
+ ums.bindNullParameter(1);
+ else
+ ums.bindInt64Parameter(1, aMessage.messageKey);
+ ums.bindInt64Parameter(2, aMessage.conversationID);
+ if (aMessage.date === null)
+ ums.bindNullParameter(3);
+ else
+ ums.bindInt64Parameter(3, aMessage.date * 1000);
+ ums.bindStringParameter(4, aMessage.headerMessageID);
+ if (aMessage._jsonText)
+ ums.bindStringParameter(5, aMessage._jsonText);
+ else
+ ums.bindNullParameter(5);
+
+ ums.executeAsync(this.trackAsync());
+
+ if (aMessage._isNew && aMessage._bodyLines) {
+ let bodyText;
+ if (aMessage._content && aMessage._content.hasContent())
+ bodyText = aMessage._content.getContentString(true);
+ else
+ bodyText = aMessage._bodyLines.join("\n");
+
+ let imts = this._insertMessageTextStatement;
+ imts.bindInt64Parameter(0, aMessage.id);
+ imts.bindStringParameter(1, aMessage._subject);
+ imts.bindStringParameter(2, bodyText);
+ if (aMessage._attachmentNames === null)
+ imts.bindNullParameter(3);
+ else
+ imts.bindStringParameter(3, aMessage._attachmentNames);
+
+ try {
+ imts.executeAsync(this.trackAsync());
+ }
+ catch(ex) {
+ throw("error executing fulltext statement... " +
+ this.asyncConnection.lastError + ": " +
+ this.asyncConnection.lastErrorString + " - " + ex);
+ }
+ }
+
+ // In completely abstract theory, this is where we would call
+ // GlodaCollectionManager.itemsModified, except that the attributes may
+ // also have changed, so it's out of our hands. (Gloda.grokNoun
+ // handles it.)
+ },
+
+ get _updateMessageLocationStatement() {
+ let statement = this._createAsyncStatement(
+ "UPDATE messages SET folderID = ?1, messageKey = ?2 WHERE id = ?3");
+ this.__defineGetter__("_updateMessageLocationStatement",
+ function() statement);
+ return this._updateMessageLocationStatement;
+ },
+
+ /**
+ * Given a list of gloda message ids, and a list of their new message keys in
+ * the given new folder location, asynchronously update the message's
+ * database locations. Also, update the in-memory representations.
+ */
+ updateMessageLocations: function gloda_ds_updateMessageLocations(aMessageIds,
+ aNewMessageKeys, aDestFolder) {
+ let statement = this._updateMessageLocationStatement;
+ let destFolderID = this._mapFolder(aDestFolder).id;
+
+ let modifiedItems = [];
+
+ for (let iMsg = 0; iMsg < aMessageIds.length; iMsg++) {
+ let id = aMessageIds[iMsg]
+ statement.bindInt64Parameter(0, destFolderID);
+ statement.bindInt64Parameter(1, aNewMessageKeys[iMsg]);
+ statement.bindInt64Parameter(2, id);
+ statement.executeAsync(this.trackAsync());
+
+ // so, if the message is currently loaded, we also need to change it up...
+ let message = GlodaCollectionManager.cacheLookupOne(
+ GlodaMessage.prototype.NOUN_ID, id);
+ if (message) {
+ message._folderID = destFolderID;
+ modifiedItems.push(message);
+ }
+ }
+
+ // if we're talking about a lot of messages, it's worth committing after
+ // this to ensure that we don't spill to disk and cause contention with
+ // synchronous reads off (this) the main thread.
+ if ((aMessageIds.length > 200) && this._transactionDepth) {
+ this._commitTransaction();
+ this._beginTransaction();
+ }
+
+ // tell the collection manager about the modified messages so it can update
+ // any existing views...
+ if (modifiedItems.length) {
+ GlodaCollectionManager.itemsModified(GlodaMessage.prototype.NOUN,
+ modifiedItems);
+ }
+ },
+
+ /**
+ * Asynchronously mutate message folder id/message keys for the given
+ * messages, indicating that we are moving them to the target folder, but
+ * don't yet know their target message keys.
+ */
+ updateMessageFoldersByKeyPurging:
+ function gloda_ds_updateMessageFoldersByKeyPurging(aSrcFolder,
+ aMessageKeys, aDestFolder) {
+ let srcFolderID = this._mapFolder(aSrcFolder).id;
+ let destFolderID = this._mapFolder(aDestFolder).id;
+
+ let sqlStr = "UPDATE messages SET folderID = ?1, \
+ messageKey = ?2 \
+ WHERE folderID = ?3 \
+ AND messageKey IN (" + aMessageKeys.join(", ") + ")";
+ let statement = this._createAsyncStatement(sqlStr, true);
+ statement.bindInt64Parameter(2, srcFolderID);
+ statement.bindInt64Parameter(0, destFolderID);
+ statement.bindNullParameter(1);
+ statement.executeAsync(this.trackAsync());
+ statement.finalize();
+
+ // if we're talking about a lot of messages, it's worth committing after
+ // this to ensure that we don't spill to disk and cause contention with
+ // synchronous reads off (this) the main thread.
+ if ((aMessageKeys.length > 200) && this._transactionDepth) {
+ this._commitTransaction();
+ this._beginTransaction();
+ }
+ },
+
+ _messageFromRow: function gloda_ds_messageFromRow(aRow) {
+ let folderId, messageKey, date, jsonText;
+ if (aRow.getTypeOfIndex(1) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
+ folderId = null;
+ else
+ folderId = aRow.getInt64(1);
+ if (aRow.getTypeOfIndex(2) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
+ messageKey = null;
+ else
+ messageKey = aRow.getInt64(2);
+ if (aRow.getTypeOfIndex(4) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
+ date = null;
+ else
+ date = new Date(aRow.getInt64(4) / 1000);
+ if (aRow.getTypeOfIndex(7) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
+ jsonText = undefined;
+ else
+ jsonText = aRow.getString(7);
+ return new GlodaMessage(this, aRow.getInt64(0), folderId, messageKey,
+ aRow.getInt64(3), null, date, aRow.getString(5),
+ aRow.getInt64(6), jsonText);
+ },
+
+ get _selectMessageByIDStatement() {
+ let statement = this._createSyncStatement(
+ "SELECT id, folderID, messageKey, conversationID, date, headerMessageID, \
+ deleted FROM messages WHERE id = ?1");
+ this.__defineGetter__("_selectMessageByIDStatement",
+ function() statement);
+ return this._selectMessageByIDStatement;
+ },
+
+ /**
+ * Synchronously retrieve the given message given its gloda message id.
+ */
+ getMessageByID: function gloda_ds_getMessageByID(aID) {
+ let message = GlodaCollectionManager.cacheLookupOne(
+ GlodaMessage.prototype.NOUN_ID, aID);
+
+ if (message === null) {
+ let smbis = this._selectMessageByIDStatement;
+
+ smbis.bindInt64Parameter(0, aID);
+ if (this._syncStep(smbis)) {
+ message = this._messageFromRow(smbis);
+ GlodaCollectionManager.itemLoaded(message);
+ }
+ smbis.reset();
+ }
+
+ return message;
+ },
+
+ get _selectMessageByLocationStatement() {
+ let statement = this._createSyncStatement(
+ "SELECT * FROM messages WHERE folderID = ?1 AND messageKey = ?2");
+ this.__defineGetter__("_selectMessageByLocationStatement",
+ function() statement);
+ return this._selectMessageByLocationStatement;
+ },
+
+ /**
+ * Synchronously retrieve the message that we believe to correspond to the
+ * given message key in the given folder.
+ * @return null on failure to locate the message, the message on success.
+ *
+ * @XXX on failure, attempt to resolve the problem through re-indexing, etc.
+ */
+ getMessageFromLocation: function gloda_ds_getMessageFromLocation(aFolder,
+ aMessageKey) {
+ this._selectMessageByLocationStatement.bindInt64Parameter(0,
+ this._mapFolder(aFolder).id);
+ this._selectMessageByLocationStatement.bindInt64Parameter(1, aMessageKey);
+
+ let message = null;
+ if (this._syncStep(this._selectMessageByLocationStatement))
+ message = this._messageFromRow(this._selectMessageByLocationStatement);
+ this._selectMessageByLocationStatement.reset();
+
+ if (message === null)
+ this._log.info("Error locating message with key=" + aMessageKey +
+ " and URI " + aFolder.URI);
+
+ return message && GlodaCollectionManager.cacheLoadUnifyOne(message);
+ },
+
+ get _selectMessageIDsByFolderStatement() {
+ let statement = this._createSyncStatement(
+ "SELECT id FROM messages WHERE folderID = ?1");
+ this.__defineGetter__("_selectMessageIDsByFolderStatement",
+ function() statement);
+ return this._selectMessageIDsByFolderStatement;
+ },
+
+ getMessageIDsByFolderID:
+ function gloda_ds_getMessageIDsFromFolderID(aFolderID) {
+ let messageIDs = [];
+
+ let smidbfs = this._selectMessageIDsByFolderStatement;
+ smidbfs.bindInt64Parameter(0, aFolderID);
+
+ while (this._syncStep(smidbfs)) {
+ messageIDs.push(smidbfs.getInt64(0));
+ }
+ smidbfs.reset();
+
+ return messageIDs;
+ },
+
+ /**
+ * Given a list of Message-ID's, return a matching list of lists of messages
+ * matching those Message-ID's. So if you pass an array with three
+ * Message-ID's ["a", "b", "c"], you would get back an array containing
+ * 3 lists, where the first list contains all the messages with a message-id
+ * of "a", and so forth. The reason a list is returned rather than null/a
+ * message is that we accept the reality that we have multiple copies of
+ * messages with the same ID.
+ * This call is asynchronous because it depends on previously created messages
+ * to be reflected in our results, which requires us to execute on the async
+ * thread where all our writes happen. This also turns out to be a
+ * reasonable thing because we could imagine pathological cases where there
+ * could be a lot of message-id's and/or a lot of messages with those
+ * message-id's.
+ */
+ getMessagesByMessageID: function gloda_ds_getMessagesByMessageID(aMessageIDs,
+ aCallback, aCallbackThis) {
+ let msgIDToIndex = {};
+ let results = [];
+ for (let iID = 0; iID < aMessageIDs.length; ++iID) {
+ let msgID = aMessageIDs[iID];
+ results.push([]);
+ msgIDToIndex[msgID] = iID;
+ }
+
+ // Unfortunately, IN doesn't work with statement binding mechanisms, and
+ // a chain of ORed tests really can't be bound unless we create one per
+ // value of N (seems silly).
+ let quotedIDs = ["'" + msgID.replace("'", "''", "g") + "'" for each
+ ([i, msgID] in Iterator(aMessageIDs))]
+ let sqlString = "SELECT * FROM messages WHERE headerMessageID IN (" +
+ quotedIDs + ")";
+
+ let nounDef = GlodaMessage.prototype.NOUN_DEF;
+ let listener = new MessagesByMessageIdCallback(msgIDToIndex, results,
+ aCallback, aCallbackThis);
+ let query = new nounDef.explicitQueryClass();
+ return this._queryFromSQLString(sqlString, [], nounDef,
+ query, listener);
+ },
+
+ get _updateMessagesMarkDeletedByFolderID() {
+ let statement = this._createAsyncStatement(
+ "UPDATE messages SET folderID = NULL, messageKey = NULL, \
+ deleted = 1 WHERE folderID = ?1");
+ this.__defineGetter__("_updateMessagesMarkDeletedByFolderID",
+ function() statement);
+ return this._updateMessagesMarkDeletedByFolderID;
+ },
+
+ markMessagesDeletedByFolderID:
+ function gloda_ds_markMessagesDeletedByFolderID(aFolderID) {
+ let statement = this._updateMessagesMarkDeletedByFolderID;
+ statement.bindInt64Parameter(0, aFolderID);
+ statement.executeAsync(this.trackAsync());
+ statement.finalize();
+ },
+
+ markMessagesDeletedByIDs: function gloda_ds_markMessagesDeletedByIDs(
+ aMessageIDs) {
+ let sqlString = "UPDATE messages SET deleted = 1 WHERE id IN (" +
+ aMessageIDs.join(",") + ")";
+
+ let statement = this._createAsyncStatement(sqlString, true);
+ statement.executeAsync(this.trackAsync());
+ statement.finalize();
+
+ // some people are inclined to deleting ridiculous numbers of messages at
+ // a time. if we are in a transaction, this has the potential to cause us
+ // to spill the transaction to disk prior to disk, resulting in a lock
+ // escalation and making any synchronous reads from the main thread need
+ // to become blocking. We don't want that, so:
+ // If we are in a transaction and there are a "lot" of messages being
+ // marked as deleted, issue a commit and then re-open the transaction.
+ if ((aMessageIDs.length > 200) && this._transactionDepth) {
+ this._commitTransaction();
+ this._beginTransaction();
+ }
+ },
+
+ get _deleteMessageByIDStatement() {
+ let statement = this._createAsyncStatement(
+ "DELETE FROM messages WHERE id = ?1");
+ this.__defineGetter__("_deleteMessageByIDStatement",
+ function() statement);
+ return this._deleteMessageByIDStatement;
+ },
+
+ deleteMessageByID: function gloda_ds_deleteMessageByID(aMessageID) {
+ // TODO: collection manager implications
+ let dmbids = this._deleteMessageByIDStatement;
+ dmbids.bindInt64Parameter(0, aMessageID);
+ dmbids.executeAsync(this.trackAsync());
+ },
+
+ get _deleteMessagesByConversationIDStatement() {
+ let statement = this._createAsyncStatement(
+ "DELETE FROM messages WHERE conversationID = ?1");
+ this.__defineGetter__("_deleteMessagesByConversationIDStatement",
+ function() statement);
+ return this._deleteMessagesByConversationIDStatement;
+ },
+
+ /**
+ * Delete messages by conversation ID. For use by the indexer's deletion
+ * logic, NOT you.
+ */
+ deleteMessagesByConversationID:
+ // TODO: collection manager implications
+ function gloda_ds_deleteMessagesByConversationID(aConversationID) {
+ let dmbcids = this._deleteMessagesByConversationIDStatement;
+ dmbcids.bindInt64Parameter(0, aConversationID);
+ dmbcids.executeAsync(this.trackAsync());
+ },
+
+ get _selectMessagesByConversationIDStatement() {
+ let statement = this._createSyncStatement(
+ "SELECT * FROM messages WHERE conversationID = ?1");
+ this.__defineGetter__("_selectMessagesByConversationIDStatement",
+ function() statement);
+ return this._selectMessagesByConversationIDStatement;
+ },
+
+ get _selectMessagesByConversationIDNoGhostsStatement() {
+ let statement = this._createSyncStatement(
+ "SELECT * FROM messages WHERE conversationID = ?1 AND \
+ folderID IS NOT NULL");
+ this.__defineGetter__("_selectMessagesByConversationIDNoGhostsStatement",
+ function() statement);
+ return this._selectMessagesByConversationIDNoGhostsStatement;
+ },
+
+ /**
+ * Retrieve all the messages belonging to the given conversation. This
+ * method is used by the indexer and the GlodaConversation class and is not
+ * intended to be used by any other code. (Most other code should probably
+ * use the GlodaConversation.messages attribute or the general purpose query
+ * mechanism.)
+ *
+ * @param aConversationID The ID of the conversation for which you want all
+ * the messages.
+ * @param aIncludeGhosts Boolean indicating whether you want 'ghost' messages
+ * (true) or not (false). 'Ghost' messages are messages that exist in the
+ * database purely for conversation tracking/threading purposes. They
+ * are markers for messages we have not yet seen yet assume must exist
+ * based on references/in-reply-to headers from non-ghost messages in our
+ * database.
+ */
+ getMessagesByConversationID: function gloda_ds_getMessagesByConversationID(
+ aConversationID, aIncludeGhosts) {
+ let statement;
+ if (aIncludeGhosts)
+ statement = this._selectMessagesByConversationIDStatement;
+ else
+ statement = this._selectMessagesByConversationIDNoGhostsStatement;
+ statement.bindInt64Parameter(0, aConversationID);
+
+ let messages = [];
+ while (this._syncStep(statement)) {
+ messages.push(this._messageFromRow(statement));
+ }
+ statement.reset();
+
+ if (messages.length)
+ GlodaCollectionManager.cacheLoadUnify(GlodaMessage.prototype.NOUN_ID,
+ messages);
+
+ return messages;
+ },
+
+ /* ********** Message Attributes ********** */
+ get _insertMessageAttributeStatement() {
+ let statement = this._createAsyncStatement(
+ "INSERT INTO messageAttributes (conversationID, messageID, attributeID, \
+ value) \
+ VALUES (?1, ?2, ?3, ?4)");
+ this.__defineGetter__("_insertMessageAttributeStatement",
+ function() statement);
+ return this._insertMessageAttributeStatement;
+ },
+
+ get _deleteMessageAttributeStatement() {
+ let statement = this._createAsyncStatement(
+ "DELETE FROM messageAttributes WHERE attributeID = ?1 AND value = ?2 \
+ AND conversationID = ?3 AND messageID = ?4");
+ this.__defineGetter__("_deleteMessageAttributeStatement",
+ function() statement);
+ return this._deleteMessageAttributeStatement;
+ },
+
+ /**
+ * Insert and remove attributes relating to a GlodaMessage. This is performed
+ * inside a pseudo-transaction (we create one if we aren't in one, using
+ * our _beginTransaction wrapper, but if we are in one, no additional
+ * meaningful semantics are added).
+ * No attempt is made to verify uniqueness of inserted attributes, either
+ * against the current database or within the provided list of attributes.
+ * The caller is responsible for ensuring that unwanted duplicates are
+ * avoided.
+ *
+ * @param aMessage The GlodaMessage the attributes belong to. This is used
+ * to provide the message id and conversation id.
+ * @param aAddDBAttributes A list of attribute tuples to add, where each tuple
+ * contains an attribute ID and a value. Lest you forget, an attribute ID
+ * corresponds to a row in the attribute definition table. The attribute
+ * definition table stores the 'parameter' for the attribute, if any.
+ * (Which is to say, our frequent Attribute-Parameter-Value triple has
+ * the Attribute-Parameter part distilled to a single attribute id.)
+ * @param aRemoveDBAttributes A list of attribute tuples to remove.
+ */
+ adjustMessageAttributes: function gloda_ds_adjustMessageAttributes(aMessage,
+ aAddDBAttributes, aRemoveDBAttributes) {
+ let imas = this._insertMessageAttributeStatement;
+ let dmas = this._deleteMessageAttributeStatement;
+ this._beginTransaction();
+ try {
+ for (let iAttrib = 0; iAttrib < aAddDBAttributes.length; iAttrib++) {
+ let attribValueTuple = aAddDBAttributes[iAttrib];
+
+ imas.bindInt64Parameter(0, aMessage.conversationID);
+ imas.bindInt64Parameter(1, aMessage.id);
+ imas.bindInt64Parameter(2, attribValueTuple[0]);
+ // use 0 instead of null, otherwise the db gets upset. (and we don't
+ // really care anyways.)
+ if (attribValueTuple[1] == null)
+ imas.bindInt64Parameter(3, 0);
+ else if (Math.floor(attribValueTuple[1]) == attribValueTuple[1])
+ imas.bindInt64Parameter(3, attribValueTuple[1]);
+ else
+ imas.bindDoubleParameter(3, attribValueTuple[1]);
+ imas.executeAsync(this.trackAsync());
+ }
+
+ for (let iAttrib = 0; iAttrib < aRemoveDBAttributes.length; iAttrib++) {
+ let attribValueTuple = aRemoveDBAttributes[iAttrib];
+
+ dmas.bindInt64Parameter(0, attribValueTuple[0]);
+ // use 0 instead of null, otherwise the db gets upset. (and we don't
+ // really care anyways.)
+ if (attribValueTuple[1] == null)
+ dmas.bindInt64Parameter(1, 0);
+ else if (Math.floor(attribValueTuple[1]) == attribValueTuple[1])
+ dmas.bindInt64Parameter(1, attribValueTuple[1]);
+ else
+ dmas.bindDoubleParameter(1, attribValueTuple[1]);
+ dmas.bindInt64Parameter(2, aMessage.conversationID);
+ dmas.bindInt64Parameter(3, aMessage.id);
+ dmas.executeAsync(this.trackAsync());
+ }
+
+ this._commitTransaction();
+ }
+ catch (ex) {
+ this._log.error("adjustMessageAttributes:" + ex.lineNumber + ": " + eX);
+ this._rollbackTransaction();
+ throw ex;
+ }
+ },
+
+ get _deleteMessageAttributesByMessageIDStatement() {
+ let statement = this._createAsyncStatement(
+ "DELETE FROM messageAttributes WHERE messageID = ?1");
+ this.__defineGetter__("_deleteMessageAttributesByMessageIDStatement",
+ function() statement);
+ return this._deleteMessageAttributesByMessageIDStatement;
+ },
+
+ /**
+ * Clear all the message attributes for a given GlodaMessage. No changes
+ * are made to the in-memory representation of the message; it is up to the
+ * caller to ensure that it handles things correctly.
+ *
+ * @param aMessage The GlodaMessage whose database attributes should be
+ * purged.
+ */
+ clearMessageAttributes: function gloda_ds_clearMessageAttributes(aMessage) {
+ if (aMessage.id != null) {
+ this._deleteMessageAttributesByMessageIDStatement.bindInt64Parameter(0,
+ aMessage.id);
+ this._deleteMessageAttributesByMessageIDStatement.executeAsync(
+ this.trackAsync());
+ }
+ },
+
+ get _selectMessageAttributesByMessageIDStatement() {
+ let statement = this._createSyncStatement(
+ "SELECT attributeID, value FROM messageAttributes \
+ WHERE messageID = ?1");
+ this.__defineGetter__("_selectMessageAttributesByMessageIDStatement",
+ function() statement);
+ return this._selectMessageAttributesByMessageIDStatement;
+ },
+
+ /**
+ * Look-up the attributes associated with the given GlodaMessage instance,
+ * returning them in APV form (a tuple of Attribute definition object,
+ * attribute Parameter, and attribute Value).
+ *
+ * @param aMessage The GlodaMessage whose attributes you want retrieved.
+ * @return An APV list of the attributes.
+ */
+ getMessageAttributes: function gloda_ds_getMessageAttributes(aMessage) {
+ // A list of [attribute def object, (attr) parameter value, attribute value]
+ let attribParamVals = []
+
+ let smas = this._selectMessageAttributesByMessageIDStatement;
+
+ smas.bindInt64Parameter(0, aMessage.id);
+ while (this._syncStep(smas)) {
+ let attributeID = smas.getInt64(0);
+ if (!(attributeID in this._attributeIDToDBDefAndParam)) {
+ this._log.error("Attribute ID " + attributeID + " not in our map!");
+ }
+ let attribAndParam = this._attributeIDToDBDefAndParam[attributeID];
+ let val = smas.getDouble(1);
+ attribParamVals.push([attribAndParam[0], attribAndParam[1], val]);
+ }
+ smas.reset();
+
+ return attribParamVals;
+ },
+
+ _stringSQLQuoter: function(aString) {
+ return "'" + aString.replace("'", "''", "g") + "'";
+ },
+ _numberQuoter: function(aNum) {
+ return aNum;
+ },
+
+ /* ===== Generic Attribute Support ===== */
+ adjustAttributes: function gloda_ds_adjustAttributes(aItem, aAddDBAttributes,
+ aRemoveDBAttributes) {
+ let nounDef = aItem.NOUN_DEF;
+ let dbMeta = nounDef._dbMeta;
+ if (dbMeta.insertAttrStatement === undefined) {
+ dbMeta.insertAttrStatement = this._createAsyncStatement(
+ "INSERT INTO " + nounDef.attrTableName +
+ " (" + nounDef.attrIDColumnName + ", attributeID, value) " +
+ " VALUES (?1, ?2, ?3)");
+ // we always create this at the same time (right here), no need to check
+ dbMeta.deleteAttrStatement = this._createAsyncStatement(
+ "DELETE FROM " + nounDef.attrTableName + " WHERE " +
+ " attributeID = ?1 AND value = ?2 AND " +
+ nounDef.attrIDColumnName + " = ?3");
+ }
+
+ let ias = dbMeta.insertAttrStatement;
+ let das = dbMeta.deleteAttrStatement;
+ this._beginTransaction();
+ try {
+ for (let iAttr = 0; iAttr < aAddDBAttributes.length; iAttr++) {
+ let attribValueTuple = aAddDBAttributes[iAttr];
+
+ ias.bindInt64Parameter(0, aItem.id);
+ ias.bindInt64Parameter(1, attribValueTuple[0]);
+ // use 0 instead of null, otherwise the db gets upset. (and we don't
+ // really care anyways.)
+ if (attribValueTuple[1] == null)
+ ias.bindInt64Parameter(2, 0);
+ else if (Math.floor(attribValueTuple[1]) == attribValueTuple[1])
+ ias.bindInt64Parameter(2, attribValueTuple[1]);
+ else
+ ias.bindDoubleParameter(2, attribValueTuple[1]);
+ ias.executeAsync(this.trackAsync());
+ }
+
+ for (let iAttr = 0; iAttr < aRemoveDBAttributes.length; iAttr++) {
+ let attribValueTuple = aRemoveDBAttributes[iAttr];
+
+ das.bindInt64Parameter(0, attribValueTuple[0]);
+ // use 0 instead of null, otherwise the db gets upset. (and we don't
+ // really care anyways.)
+ if (attribValueTuple[1] == null)
+ das.bindInt64Parameter(1, 0);
+ else if (Math.floor(attribValueTuple[1]) == attribValueTuple[1])
+ das.bindInt64Parameter(1, attribValueTuple[1]);
+ else
+ das.bindDoubleParameter(1, attribValueTuple[1]);
+ das.bindInt64Parameter(2, aItem.id);
+ das.executeAsync(this.trackAsync());
+ }
+
+ this._commitTransaction();
+ }
+ catch (ex) {
+ this._log.error("adjustAttributes:" + ex.lineNumber + ": " + eX);
+ this._rollbackTransaction();
+ throw ex;
+ }
+ },
+
+ clearAttributes: function gloda_ds_clearAttributes(aItem) {
+ let nounDef = aItem.NOUN_DEF;
+ let dbMeta = nounMeta._dbMeta;
+ if (dbMeta.clearAttrStatement === undefined) {
+ dbMeta.clearAttrStatement = this._createAsyncStatement(
+ "DELETE FROM " + nounDef.attrTableName + " WHERE " +
+ nounDef.attrIDColumnName + " = ?1");
+ }
+
+ if (aItem.id != null) {
+ dbMeta.clearAttrStatement.bindInt64Parameter(0, aItem.id);
+ dbMeta.clearAttrStatement.executeAsync(this.trackAsync());
+ }
+ },
+
+ /**
+ * escapeStringForLIKE is only available on statements, and sometimes we want
+ * to use it before we create our statement, so we create a statement just
+ * for this reason.
+ */
+ get _escapeLikeStatement() {
+ let statement = this._createAsyncStatement("SELECT 0");
+ this.__defineGetter__("_escapeLikeStatement", function() statement);
+ return this._escapeLikeStatement;
+ },
+
+ _convertToDBValuesAndGroupByAttributeID:
+ function gloda_ds__convertToDBValuesAndGroupByAttributeID(aAttrDef,
+ aValues) {
+ let objectNounDef = aAttrDef.objectNounDef;
+ if (!objectNounDef.usesParameter) {
+ let dbValues = [];
+ for (let iValue = 0; iValue < aValues.length; iValue++) {
+ let dbValue = objectNounDef.toParamAndValue(aValues[iValue])[1];
+ if (dbValue != null)
+ dbValues.push(dbValue);
+ }
+ yield [aAttrDef.special ? undefined : aAttrDef.id, dbValues];
+ return;
+ }
+
+ let curParam, attrID, dbValues;
+ let attrDBDef = aAttrDef.dbDef;
+ for (let iValue = 0; iValue < aValues.length; iValue++) {
+ let [dbParam, dbValue] = objectNounDef.toParamAndValue(aValues[iValue]);
+ if (curParam === undefined) {
+ curParam = dbParam;
+ attrID = attrDBDef.bindParameter(curParam);
+ if (dbValue != null)
+ dbValues = [dbValue];
+ else
+ dbValues = [];
+ }
+ else if (curParam == dbParam) {
+ if (dbValue != null)
+ dbValues.push(dbValue);
+ }
+ else {
+ yield [attrID, dbValues];
+ curParam = dbParam;
+ attrID = attrDBDef.bindParameter(curParam);
+ if (dbValue != null)
+ dbValues = [dbValue];
+ else
+ dbValues = [];
+ }
+ }
+ if (dbValues !== undefined)
+ yield [attrID, dbValues];
+ },
+
+ _convertRangesToDBStringsAndGroupByAttributeID:
+ function gloda_ds__convertRangesToDBStringsAndGroupByAttributeID(aAttrDef,
+ aValues, aValueColumnName) {
+ let objectNounDef = aAttrDef.objectNounDef;
+ if (!objectNounDef.usesParameter) {
+ let dbStrings = [];
+ for (let iValue = 0; iValue < aValues.length; iValue++) {
+ let [lowerVal, upperVal] = aValues[iValue];
+ // they both can't be null. that is the law.
+ if (lowerVal == null)
+ dbStrings.push(aValueColumnName + " <= " +
+ objectNounDef.toParamAndValue(upperVal)[1]);
+ else if (upperVal == null)
+ dbStrings.push(aValueColumnName + " >= " +
+ objectNounDef.toParamAndValue(lowerVal)[1]);
+ else // no one is null!
+ dbStrings.push(aValueColumnName + " BETWEEN " +
+ objectNounDef.toParamAndValue(lowerVal)[1] + " AND " +
+ objectNounDef.toParamAndValue(upperVal)[1]);
+ }
+ yield [aAttrDef.special ? undefined : aAttrDef.id, dbStrings];
+ return;
+ }
+
+ let curParam, attrID, dbStrings;
+ let attrDBDef = aAttrDef.dbDef;
+ for (let iValue = 0; iValue < aValues.length; iValue++) {
+ let [lowerVal, upperVal] = aValues[iValue];
+
+ let dbString, dbParam, lowerDBVal, upperDBVal;
+ // they both can't be null. that is the law.
+ if (lowerVal == null) {
+ [dbParam, upperDBVal] = objectNounDef.toParamAndValue(upperVal);
+ dbString = aValueColumnName + " <= " + upperDBVal;
+ }
+ else if (upperVal == null) {
+ [dbParam, lowerDBVal] = objectNounDef.toParamAndValue(lowerVal);
+ dbString = aValueColumnName + " >= " + lowerDBVal;
+ }
+ else { // no one is null!
+ [dbParam, lowerDBVal] = objectNounDef.toParamAndValue(lowerVal);
+ dbString = aValueColumnName + " BETWEEN " + lowerDBVal + " AND " +
+ objectNounDef.toParamAndValue(upperVal)[1];
+ }
+
+ if (curParam === undefined) {
+ curParam = dbParam;
+ attrID = attrDBDef.bindParameter(curParam);
+ dbStrings = [dbString];
+ }
+ else if (curParam === dbParam) {
+ dbStrings.push(dbString);
+ }
+ else {
+ yield [attrID, dbStrings];
+ curParam = dbParam;
+ attrID = attrDBDef.bindParameter(curParam);
+ dbStrings = [dbString];
+ }
+ }
+ if (dbStrings !== undefined)
+ yield [attrID, dbStrings];
+ },
+
+ /**
+ * Perform a database query given a GlodaQueryClass instance that specifies
+ * a set of constraints relating to the noun type associated with the query.
+ * A GlodaCollection is returned containing the results of the look-up.
+ * By default the collection is "live", and will mutate (generating events to
+ * its listener) as the state of the database changes.
+ * This functionality is made user/extension visible by the Query's
+ * getCollection (asynchronous).
+ */
+ queryFromQuery: function gloda_ds_queryFromQuery(aQuery, aListener,
+ aListenerData, aExistingCollection, aMasterCollection) {
+ // when changing this method, be sure that GlodaQuery's testMatch function
+ // likewise has its changes made.
+ let nounDef = aQuery._nounDef;
+
+ let whereClauses = [];
+ let unionQueries = [aQuery].concat(aQuery._unions);
+ let boundArgs = [];
+
+ for (let iUnion = 0; iUnion < unionQueries.length; iUnion++) {
+ let curQuery = unionQueries[iUnion];
+ let selects = [];
+
+ let lastConstraintWasSpecial = false;
+ let curConstraintIsSpecial;
+
+ for (let iConstraint = 0; iConstraint < curQuery._constraints.length;
+ iConstraint++) {
+ let constraint = curQuery._constraints[iConstraint];
+ let [constraintType, attrDef] = constraint;
+ let constraintValues = constraint.slice(2);
+
+ let idColumnName, tableColumnName;
+ if (constraintType == this.kConstraintIdIn) {
+ // we don't need any of the next cases' setup code, and we especially
+ // would prefer that attrDef isn't accessed since it's null for us.
+ }
+ else if (attrDef.special) {
+ tableName = nounDef.tableName;
+ idColumnName = "id"; // canonical id for a table is "id".
+ valueColumnName = attrDef.specialColumnName;
+ curConstraintIsSpecial = true;
+ }
+ else {
+ tableName = nounDef.attrTableName;
+ idColumnName = nounDef.attrIDColumnName;
+ valueColumnName = "value";
+ curConstraintIsSpecial = false;
+ }
+
+ let select = null, test = null, bindArgs = null;
+ if (constraintType === this.kConstraintIdIn) {
+ // this is somewhat of a trick. this does mean that this can be the
+ // only constraint. Namely, our idiom is:
+ // SELECT * FROM blah WHERE id IN (a INTERSECT b INTERSECT c)
+ // but if we only have 'a', then that becomes "...IN (a)", and if
+ // 'a' is not a select but a list of id's... tricky, no?
+ select = constraintValues.join(",");
+ }
+ else if (constraintType === this.kConstraintIn) {
+ let clauses = [];
+ for each ([attrID, values] in
+ this._convertToDBValuesAndGroupByAttributeID(attrDef,
+ constraintValues)) {
+ let clausePart;
+ if (attrID !== undefined)
+ clausePart = "(attributeID = " + attrID +
+ (values.length ? " AND " : "");
+ else
+ clausePart = "(";
+ if (values.length)
+ clausePart += valueColumnName + " IN (" + values.join(",") + "))";
+ else
+ clausePart += ")";
+ clauses.push(clausePart);
+ }
+ test = clauses.join(" OR ");
+ }
+ else if (constraintType === this.kConstraintRanges) {
+ let clauses = [];
+ for each ([attrID, dbStrings] in
+ this._convertRangesToDBStringsAndGroupByAttributeID(attrDef,
+ constraintValues, valueColumnName)) {
+ if (attrID !== undefined)
+ clauses.push("(attributeID = " + attrID +
+ " AND (" + dbStrings.join(" OR ") + "))");
+ else
+ clauses.push("(" + dbStrings.join(" OR ") + ")");
+ }
+ test = clauses.join(" OR ");
+ }
+ else if (constraintType === this.kConstraintEquals) {
+ let clauses = [];
+ for each ([attrID, values] in
+ this._convertToDBValuesAndGroupByAttributeID(attrDef,
+ constraintValues)) {
+ if (attrID !== undefined)
+ clauses.push("(attributeID = " + attrID +
+ " AND (" + [valueColumnName + " = ?" for each
+ (value in values)].join(" OR ") + "))");
+ else
+ clauses.push("(" + [valueColumnName + " = ?" for each
+ (value in values)].join(" OR ") + ")");
+ boundArgs.push.apply(boundArgs, values);
+ }
+ test = clauses.join(" OR ");
+ }
+ else if (constraintType === this.kConstraintStringLike) {
+ likePayload = '';
+ for each (let [iValuePart, valuePart] in Iterator(constraintValues)) {
+ if (typeof valuePart == "string")
+ likePayload += this._escapeLikeStatement.escapeStringForLIKE(
+ valuePart, "/");
+ else
+ likePayload += "%";
+ }
+ test = valueColumnName + " LIKE ? ESCAPE '/'";
+ boundArgs.push(likePayload);
+ }
+ else if (constraintType === this.kConstraintFulltext) {
+ let matchStr = constraintValues[0];
+ select = "SELECT docid FROM " + nounDef.tableName + "Text" +
+ " WHERE " + attrDef.specialColumnName + " MATCH ?";
+ boundArgs.push(matchStr);
+ }
+
+ if (curConstraintIsSpecial && lastConstraintWasSpecial && test) {
+ selects[selects.length-1] += " AND " + test;
+ }
+ else if (select)
+ selects.push(select)
+ else if (test) {
+ select = "SELECT " + idColumnName + " FROM " + tableName + " WHERE " +
+ test;
+ selects.push(select);
+ }
+ else
+ this._log.warning("Unable to translate constraint of type " +
+ constraintType + " on attribute bound as " + aAttrDef.boundName);
+
+ lastConstraintWasSpecial = curConstraintIsSpecial;
+ }
+
+ if (selects.length)
+ whereClauses.push("id IN (" + selects.join(" INTERSECT ") + ")");
+ }
+
+ let sqlString = "SELECT * FROM " + nounDef.tableName;
+ if (whereClauses.length)
+ sqlString += " WHERE " + whereClauses.join(" OR ");
+
+ if (aQuery._order.length) {
+ let orderClauses = [];
+ for (let [, colName] in Iterator(aQuery._order)) {
+ if (colName[0] == "-")
+ orderClauses.push(colName.substring(1) + " DESC");
+ else
+ orderClauses.push(colName + " ASC");
+ }
+ sqlString += " ORDER BY " + orderClauses.join(", ");
+ }
+
+ if (aQuery._limit) {
+ sqlString += " LIMIT ?";
+ boundArgs.push(aQuery._limit);
+ }
+
+ this._log.debug("QUERY FROM QUERY: " + sqlString + " ARGS: " + boundArgs);
+
+ return this._queryFromSQLString(sqlString, boundArgs, nounDef, aQuery,
+ aListener, aListenerData, aExistingCollection, aMasterCollection);
+ },
+
+ _queryFromSQLString: function gloda_ds__queryFromSQLString(aSqlString,
+ aBoundArgs, aNounDef, aQuery, aListener, aListenerData,
+ aExistingCollection, aMasterCollection) {
+ let statement = this._createAsyncStatement(aSqlString, true);
+ for (let [iBinding, bindingValue] in Iterator(aBoundArgs)) {
+ this._bindVariant(statement, iBinding, bindingValue);
+ }
+
+ let collection;
+ if (aExistingCollection)
+ collection = aExistingCollection;
+ else {
+ collection = new GlodaCollection(aNounDef, [], aQuery, aListener,
+ aMasterCollection);
+ GlodaCollectionManager.registerCollection(collection);
+ // we don't want to overwrite the existing listener or its data, but this
+ // does raise the question about what should happen if we get passed in
+ // a different listener and/or data.
+ if (aListenerData !== undefined)
+ collection.data = aListenerData;
+ }
+ if (aListenerData) {
+ if (collection.dataStack)
+ collection.dataStack.push(aListenerData)
+ else
+ collection.dataStack = [aListenerData];
+ }
+
+ statement.executeAsync(new QueryFromQueryCallback(statement, aNounDef,
+ collection));
+ statement.finalize();
+ return collection;
+ },
+
+ /**
+ *
+ *
+ */
+ loadNounItem: function gloda_ds_loadNounItem(aItem, aReferencesByNounID,
+ aInverseReferencesByNounID) {
+ let attribIDToDBDefAndParam = this._attributeIDToDBDefAndParam;
+
+ let hadDeps = aItem._deps != null;
+ let deps = aItem._deps || {};
+ let hasDeps = false;
+
+ //this._log.debug(" hadDeps: " + hadDeps + " deps: " +
+ // Log4Moz.enumerateProperties(deps).join(","));
+
+ for each (let [, attrib] in Iterator(aItem.NOUN_DEF.specialLoadAttribs)) {
+ let objectNounDef = attrib.objectNounDef;
+
+ if (attrib.special === this.kSpecialColumnChildren) {
+ let invReferences = aInverseReferencesByNounID[objectNounDef.id];
+ if (invReferences === undefined)
+ invReferences = aInverseReferencesByNounID[objectNounDef.id] = {};
+ // only contribute if it's not already pending or there
+ if (!(attrib.id in deps) && aItem[attrib.storageAttributeName] == null){
+ //this._log.debug(" Adding inv ref for: " + aItem.id);
+ if (!(aItem.id in invReferences))
+ invReferences[aItem.id] = null;
+ deps[attrib.id] = null;
+ hasDeps = true;
+ }
+ }
+ else if (attrib.special === this.kSpecialColumnParent) {
+ let references = aReferencesByNounID[objectNounDef.id];
+ if (references === undefined)
+ references = aReferencesByNounID[objectNounDef.id] = {};
+ // nothing to contribute if it's already there
+ if (!(attrib.id in deps) &&
+ aItem[attrib.valueStorageAttributeName] == null) {
+ let parentID = aItem[attrib.idStorageAttributeName];
+ if (!(parentID in references))
+ references[parentID] = null;
+ //this._log.debug(" Adding parent ref for: " +
+ // aItem[attrib.idStorageAttributeName]);
+ deps[attrib.id] = null;
+ hasDeps = true;
+ }
+ else {
+ this._log.debug(" paranoia value storage: " + aItem[attrib.valueStorageAttributeName]);
+ }
+ }
+ }
+
+ // bail here if arbitrary values are not allowed, there just is no
+ // encoded json, or we already had dependencies for this guy, implying
+ // the json pass has already been performed
+ if (!aItem.NOUN_DEF.allowsArbitraryAttrs || !aItem._jsonText || hadDeps) {
+ if (hasDeps)
+ aItem._deps = deps;
+ return hasDeps;
+ }
+
+ //this._log.debug(" load json: " + aItem._jsonText);
+ let jsonDict = this._json.decode(aItem._jsonText);
+ delete aItem._jsonText;
+
+ // Iterate over the attributes on the item
+ for each (let [attribId, jsonValue] in Iterator(jsonDict)) {
+ // find the attribute definition that corresponds to this key
+ let dbAttrib = attribIDToDBDefAndParam[attribId][0];
+ // the attribute should only fail to exist if an extension was removed
+ if (dbAttrib === undefined)
+ continue;
+
+ let attrib = dbAttrib.attrDef;
+ let objectNounDef = attrib.objectNounDef;
+
+ // if it has a tableName member, then it's a persistent object that needs
+ // to be loaded, which also means we need to hold it in a collection
+ // owned by our collection.
+ if (objectNounDef.tableName) {
+ let references = aReferencesByNounID[objectNounDef.id];
+ if (references === undefined)
+ references = aReferencesByNounID[objectNounDef.id] = {};
+
+ if (attrib.singular) {
+ if (!(jsonValue in references))
+ references[jsonValue] = null;
+ }
+ else {
+ for each (let [, anID] in Iterator(jsonValue)) {
+ if (!(anID in references))
+ references[anID] = null;
+ }
+ }
+
+ deps[attribId] = jsonValue;
+ hasDeps = true;
+ }
+ /* if it has custom contribution logic, use it */
+ else if (objectNounDef.contributeObjDependencies) {
+ if (objectNounDef.contributeObjDependencies(jsonValue,
+ aReferencesByNounID, aInverseReferencesByNounID)) {
+ deps[attribId] = jsonValue;
+ hasDeps = true;
+ }
+ else // just propagate the value, it's some form of simple sentinel
+ aItem[attrib.boundName] = jsonValue;
+ }
+ // otherwise, the value just needs to be de-persisted, or not
+ else if (objectNounDef.fromJSON) {
+ if (attrib.singular)
+ aItem[attrib.boundName] = objectNounDef.fromJSON(jsonValue);
+ else
+ aItem[attrib.boundName] = [objectNounDef.fromJSON(val) for each
+ ([, val] in Iterator(jsonValue))];
+ }
+ // it's fine as is
+ else
+ aItem[attrib.boundName] = jsonValue;
+ }
+
+ if (hasDeps)
+ aItem._deps = deps;
+ return hasDeps;
+ },
+
+ loadNounDeferredDeps: function gloda_ds_loadNounDeferredDeps(aItem,
+ aReferencesByNounID, aInverseReferencesByNounID) {
+ if (aItem._deps === undefined)
+ return;
+
+ //this._log.debug(" loading deferred, deps: " +
+ // Log4Moz.enumerateProperties(aItem._deps).join(","));
+
+
+ let attribIDToDBDefAndParam = this._attributeIDToDBDefAndParam;
+
+ for (let [attribId, jsonValue] in Iterator(aItem._deps)) {
+ let dbAttrib = attribIDToDBDefAndParam[attribId][0];
+ let attrib = dbAttrib.attrDef;
+
+ let objectNounDef = attrib.objectNounDef;
+ let references = aReferencesByNounID[objectNounDef.id];
+ if (attrib.special) {
+ if (attrib.special === this.kSpecialColumnChildren) {
+ let inverseReferences = aInverseReferencesByNounID[objectNounDef.id];
+ //this._log.info("inverse assignment: " + objectNounDef.id +
+ // " of " + aItem.id)
+ aItem[attrib.storageAttributeName] = inverseReferences[aItem.id];
+ }
+ else if (attrib.special === this.kSpecialColumnParent) {
+ //this._log.info("parent column load: " + objectNounDef.id +
+ // " storage value: " + aItem[attrib.idStorageAttributeName]);
+ aItem[attrib.valueStorageAttributeName] =
+ references[aItem[attrib.idStorageAttributeName]];
+ }
+ }
+ else if (objectNounDef.tableName) {
+ //this._log.info("trying to load: " + objectNounDef.id + " refs: " +
+ // jsonValue + ": " + Log4Moz.enumerateProperties(jsonValue).join(","));
+ if (attrib.singular)
+ aItem[attrib.boundName] = references[jsonValue];
+ else
+ aItem[attrib.boundName] = [references[val] for each
+ ([, val] in Iterator(jsonValue))];
+ }
+ else if (objectNounDef.contributeObjDependencies) {
+ aItem[attrib.boundName] =
+ objectNounDef.resolveObjDependencies(jsonValue, aReferencesByNounID,
+ aInverseReferencesByNounID);
+ }
+ // there is no other case
+ }
+
+ delete aItem._deps;
+ },
+
+ /* ********** Contact ********** */
+ _nextContactId: 1,
+
+ _populateContactManagedId: function () {
+ let stmt = this._createSyncStatement("SELECT MAX(id) FROM contacts", true);
+ if (stmt.executeStep()) { // no chance of this SQLITE_BUSY on this call
+ this._nextContactId = stmt.getInt64(0) + 1;
+ }
+ stmt.finalize();
+ },
+
+ get _insertContactStatement() {
+ let statement = this._createAsyncStatement(
+ "INSERT INTO contacts (id, directoryUUID, contactUUID, name, popularity,\
+ frecency, jsonAttributes) \
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)");
+ this.__defineGetter__("_insertContactStatement", function() statement);
+ return this._insertContactStatement;
+ },
+
+ createContact: function gloda_ds_createContact(aDirectoryUUID, aContactUUID,
+ aName, aPopularity, aFrecency) {
+ let contactID = this._nextContactId++;
+ this._log.debug("createContact: " + contactID + ": " + aName);
+ let contact = new GlodaContact(this, contactID,
+ aDirectoryUUID, aContactUUID, aName,
+ aPopularity, aFrecency);
+ return contact;
+ },
+
+ insertContact: function gloda_ds_insertContact(aContact) {
+ let ics = this._insertContactStatement;
+ ics.bindInt64Parameter(0, aContact.id);
+ if (aContact.directoryUUID == null)
+ ics.bindNullParameter(1);
+ else
+ ics.bindStringParameter(1, aContact.directoryUUID);
+ if (aContact.contactUUID == null)
+ ics.bindNullParameter(2);
+ else
+ ics.bindStringParameter(2, aContact.contactUUID);
+ ics.bindStringParameter(3, aContact.name);
+ ics.bindInt64Parameter(4, aContact.popularity);
+ ics.bindInt64Parameter(5, aContact.frecency);
+ if (aContact._jsonText)
+ ics.bindStringParameter(6, aContact._jsonText);
+ else
+ ics.bindNullParameter(6);
+
+ ics.executeAsync(this.trackAsync());
+ this._log.debug("insertContact: " + aContact.id + ":" + aContact.name);
+
+ return aContact;
+ },
+
+ get _updateContactStatement() {
+ let statement = this._createAsyncStatement(
+ "UPDATE contacts SET directoryUUID = ?1, \
+ contactUUID = ?2, \
+ name = ?3, \
+ popularity = ?4, \
+ frecency = ?5, \
+ jsonAttributes = ?6 \
+ WHERE id = ?7");
+ this.__defineGetter__("_updateContactStatement", function() statement);
+ return this._updateContactStatement;
+ },
+
+ updateContact: function gloda_ds_updateContact(aContact) {
+ let ucs = this._updateContactStatement;
+ ucs.bindInt64Parameter(6, aContact.id);
+ ucs.bindStringParameter(0, aContact.directoryUUID);
+ ucs.bindStringParameter(1, aContact.contactUUID);
+ ucs.bindStringParameter(2, aContact.name);
+ ucs.bindInt64Parameter(3, aContact.popularity);
+ ucs.bindInt64Parameter(4, aContact.frecency);
+ if (aContact._jsonText)
+ ucs.bindStringParameter(5, aContact._jsonText);
+ else
+ ucs.bindNullParameter(5);
+
+ ucs.executeAsync(this.trackAsync());
+ },
+
+ _contactFromRow: function gloda_ds_contactFromRow(aRow) {
+ let directoryUUID, contactUUID, jsonText;
+ if (aRow.getTypeOfIndex(1) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
+ directoryUUID = null;
+ else
+ directoryUUID = aRow.getString(1);
+ if (aRow.getTypeOfIndex(2) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
+ contactUUID = null;
+ else
+ contactUUID = aRow.getString(2);
+ if (aRow.getTypeOfIndex(6) == Ci.mozIStorageValueArray.VALUE_TYPE_NULL)
+ jsonText = undefined;
+ else
+ jsonText = aRow.getString(6);
+
+ return new GlodaContact(this, aRow.getInt64(0), directoryUUID,
+ contactUUID, aRow.getString(5),
+ aRow.getInt64(3), aRow.getInt64(4), jsonText);
+ },
+
+ get _selectContactByIDStatement() {
+ let statement = this._createSyncStatement(
+ "SELECT * FROM contacts WHERE id = ?1");
+ this.__defineGetter__("_selectContactByIDStatement",
+ function() statement);
+ return this._selectContactByIDStatement;
+ },
+
+ getContactByID: function gloda_ds_getContactByID(aContactID) {
+ let contact = GlodaCollectionManager.cacheLookupOne(
+ GlodaContact.prototype.NOUN_ID, aContactID);
+
+ if (contact === null) {
+ let scbi = this._selectContactByIDStatement;
+ scbi.bindInt64Parameter(0, aContactID);
+ if (this._syncStep(scbi)) {
+ contact = this._contactFromRow(scbi);
+ GlodaCollectionManager.itemLoaded(contact);
+ }
+ scbi.reset();
+ }
+
+ return contact;
+ },
+
+ /* ********** Identity ********** */
+ /** next identity id, managed for async use reasons. */
+ _nextIdentityId: 1,
+ _populateIdentityManagedId: function () {
+ let stmt = this._createSyncStatement(
+ "SELECT MAX(id) FROM identities", true);
+ if (stmt.executeStep()) { // no chance of this SQLITE_BUSY on this call
+ this._nextIdentityId = stmt.getInt64(0) + 1;
+ }
+ stmt.finalize();
+ },
+
+ get _insertIdentityStatement() {
+ let statement = this._createAsyncStatement(
+ "INSERT INTO identities (id, contactID, kind, value, description, relay) \
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6)");
+ this.__defineGetter__("_insertIdentityStatement", function() statement);
+ return this._insertIdentityStatement;
+ },
+
+ createIdentity: function gloda_ds_createIdentity(aContactID, aContact, aKind,
+ aValue, aDescription,
+ aIsRelay) {
+ let identityID = this._nextIdentityId++;
+ let iis = this._insertIdentityStatement;
+ iis.bindInt64Parameter(0, identityID);
+ iis.bindInt64Parameter(1, aContactID);
+ iis.bindStringParameter(2, aKind);
+ iis.bindStringParameter(3, aValue);
+ iis.bindStringParameter(4, aDescription);
+ iis.bindInt64Parameter(5, aIsRelay ? 1 : 0);
+ iis.executeAsync(this.trackAsync());
+
+ let identity = new GlodaIdentity(this, identityID,
+ aContactID, aContact, aKind, aValue,
+ aDescription, aIsRelay);
+ GlodaCollectionManager.itemsAdded(identity.NOUN_ID, [identity]);
+ return identity;
+ },
+
+ _identityFromRow: function gloda_ds_identityFromRow(aRow) {
+ return new GlodaIdentity(this, aRow.getInt64(0), aRow.getInt64(1), null,
+ aRow.getString(2), aRow.getString(3),
+ aRow.getString(4),
+ aRow.getInt32(5) ? true : false);
+ },
+
+ get _selectIdentityByKindValueStatement() {
+ let statement = this._createSyncStatement(
+ "SELECT * FROM identities WHERE kind = ?1 AND value = ?2");
+ this.__defineGetter__("_selectIdentityByKindValueStatement",
+ function() statement);
+ return this._selectIdentityByKindValueStatement;
+ },
+
+ /** Lookup an identity by kind and value. Ex: (email, foo@bar.com) */
+ getIdentity: function gloda_ds_getIdentity(aKind, aValue) {
+ let identity = GlodaCollectionManager.cacheLookupOneByUniqueValue(
+ GlodaIdentity.prototype.NOUN_ID, aKind + "@" + aValue);
+
+ let ibkv = this._selectIdentityByKindValueStatement;
+ ibkv.bindStringParameter(0, aKind);
+ ibkv.bindStringParameter(1, aValue);
+ if (this._syncStep(ibkv)) {
+ identity = this._identityFromRow(ibkv);
+ GlodaCollectionManager.itemLoaded(identity);
+ }
+ ibkv.reset();
+
+ return identity;
+ },
+
+ get _selectIdentityByIDStatement() {
+ let statement = this._createSyncStatement(
+ "SELECT * FROM identities WHERE id = ?1");
+ this.__defineGetter__("_selectIdentityByIDStatement",
+ function() statement);
+ return this._selectIdentityByIDStatement;
+ },
+
+ getIdentityByID: function gloda_ds_getIdentityByID(aID) {
+ let identity = GlodaCollectionManager.cacheLookupOne(
+ GlodaIdentity.prototype.NOUN_ID, aID);
+
+ if (identity === null) {
+ let sibis = this._selectIdentityByIDStatement;
+ sibis.bindInt64Parameter(0, aID);
+ if (this._syncStep(sibis)) {
+ identity = this._identityFromRow(sibis);
+ GlodaCollectionManager.itemLoaded(identity);
+ }
+ sibis.reset();
+ }
+
+ return identity;
+ },
+
+ get _selectIdentityByContactIDStatement() {
+ let statement = this._createSyncStatement(
+ "SELECT * FROM identities WHERE contactID = ?1");
+ this.__defineGetter__("_selectIdentityByContactIDStatement",
+ function() statement);
+ return this._selectIdentityByContactIDStatement;
+ },
+
+ getIdentitiesByContactID: function gloda_ds_getIdentitiesByContactID(
+ aContactID) {
+ let sibcs = this._selectIdentityByContactIDStatement;
+
+ sibcs.bindInt64Parameter(0, aContactID);
+
+ let identities = [];
+ while (this._syncStep(sibcs)) {
+ identities.push(this._identityFromRow(sibcs));
+ }
+ sibcs.reset();
+
+ if (identities.length)
+ GlodaCollectionManager.cacheLoadUnify(GlodaIdentity.prototype.NOUN_ID,
+ identities);
+ return identities;
+ },
+};
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/everybody.js
@@ -0,0 +1,90 @@
+/* ***** 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 Thunderbird Global Database.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Messaging, Inc.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Andrew Sutherland <asutherland@asutherland.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 ***** */
+
+const EXPORTED_SYMBOLS = [];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gloda/modules/log4moz.js");
+const LOG = Log4Moz.Service.getLogger("gloda.everybody");
+
+var importNS = {};
+var strtab = null;
+
+function loadModule(aModuleURI, aNSContrib) {
+ if (strtab === null) {
+ let bundleService = Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService);
+ strtab = bundleService.createBundle("chrome://gloda/locale/gloda.properties");
+ LOG.debug("string bundle: " + strtab);
+ }
+
+ try {
+ LOG.info("... loading " + aModuleURI);
+ Cu.import(aModuleURI, importNS);
+ }
+ catch (ex) {
+ LOG.error("!!! error loading " + aModuleURI);
+ LOG.error("(" + ex.fileName + ":" + ex.lineNumber + ") " + ex);
+ return false;
+ }
+ LOG.info("+++ loaded " + aModuleURI);
+
+ if (aNSContrib) {
+ try {
+ importNS[aNSContrib].init(strtab);
+ }
+ catch (ex) {
+ LOG.error("!!! error initializing " + aModuleURI);
+ LOG.error("(" + ex.fileName + ":" + ex.lineNumber + ") " + ex);
+ return false;
+ }
+ LOG.info("+++ inited " + aModuleURI);
+ }
+ return true;
+}
+
+loadModule("resource://gloda/modules/fundattr.js", "GlodaFundAttr");
+loadModule("resource://gloda/modules/explattr.js", "GlodaExplicitAttr");
+
+loadModule("resource://gloda/modules/noun_tag.js");
+loadModule("resource://gloda/modules/noun_freetag.js");
+loadModule("resource://gloda/modules/noun_mimetype.js");
+loadModule("resource://gloda/modules/index_ab.js", "GlodaABAttrs");
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/explattr.js
@@ -0,0 +1,165 @@
+/* ***** 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 Thunderbird Global Database.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Messaging, Inc.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Andrew Sutherland <asutherland@asutherland.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 ***** */
+
+/*
+ * This file provides the "explicit attribute" provider for messages. It is
+ * concerned with attributes that are the result of user actions. For example,
+ * whether a message is starred (flagged), message tags, whether it is
+ * read/unread, etc.
+ */
+
+EXPORTED_SYMBOLS = ['GlodaExplicitAttr'];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gloda/modules/log4moz.js");
+
+Cu.import("resource://gloda/modules/utils.js");
+Cu.import("resource://gloda/modules/gloda.js");
+Cu.import("resource://gloda/modules/noun_tag.js");
+
+
+const EXT_BUILTIN = "built-in";
+const FA_TAG = "TAG";
+const FA_STAR = "STAR";
+const FA_READ = "READ";
+
+/**
+ * @namespace Explicit attribute provider. Indexes/defines attributes that are
+ * explicitly a result of user action. This dubiously includes marking a
+ * message as read.
+ */
+var GlodaExplicitAttr = {
+ providerName: "gloda.explattr",
+ _log: null,
+ _strBundle: null,
+ _msgTagService: null,
+
+ init: function gloda_explattr_init(aStrBundle) {
+ this._log = Log4Moz.Service.getLogger("gloda.explattr");
+ this._strBundle = aStrBundle;
+
+ this._msgTagService = Cc["@mozilla.org/messenger/tagservice;1"].
+ getService(Ci.nsIMsgTagService);
+
+ try {
+ this.defineAttributes();
+ }
+ catch (ex) {
+ this._log.error("Error in init: " + ex);
+ throw ex;
+ }
+ },
+
+ _attrTag: null,
+ _attrStar: null,
+ _attrRead: null,
+
+ defineAttributes: function() {
+ // Tag
+ this._attrTag = Gloda.defineAttribute({
+ provider: this,
+ extensionName: Gloda.BUILT_IN,
+ attributeType: Gloda.kAttrExplicit,
+ attributeName: "tag",
+ bindName: "tags",
+ singular: false,
+ subjectNouns: [Gloda.NOUN_MESSAGE],
+ objectNoun: Gloda.NOUN_TAG,
+ parameterNoun: null,
+ // Property change notifications that we care about:
+ propertyChanges: ["keywords"],
+ }); // not-tested
+
+ // Star
+ this._attrStar = Gloda.defineAttribute({
+ provider: this,
+ extensionName: Gloda.BUILT_IN,
+ attributeType: Gloda.kAttrExplicit,
+ attributeName: "star",
+ bindName: "starred",
+ singular: true,
+ subjectNouns: [Gloda.NOUN_MESSAGE],
+ objectNoun: Gloda.NOUN_BOOLEAN,
+ parameterNoun: null,
+ }); // tested-by: test_attributes_explicit
+ // Read/Unread
+ this._attrRead = Gloda.defineAttribute({
+ provider: this,
+ extensionName: Gloda.BUILT_IN,
+ attributeType: Gloda.kAttrExplicit,
+ attributeName: "read",
+ singular: true,
+ subjectNouns: [Gloda.NOUN_MESSAGE],
+ objectNoun: Gloda.NOUN_BOOLEAN,
+ parameterNoun: null,
+ }); // tested-by: test_attributes_explicit
+
+ },
+
+ process: function Gloda_explattr_process(aGlodaMessage, aRawReps, aIsNew,
+ aCallbackHandle) {
+ let aMsgHdr = aRawReps.header;
+
+ aGlodaMessage.starred = aMsgHdr.isFlagged;
+ aGlodaMessage.read = aMsgHdr.isRead;
+
+ let tags = aGlodaMessage.tags = [];
+
+ // -- Tag
+ // build a map of the keywords
+ let keywords = aMsgHdr.getStringProperty("keywords");
+ let keywordList = keywords.split(' ');
+ let keywordMap = {};
+ for (let iKeyword = 0; iKeyword < keywordList.length; iKeyword++) {
+ let keyword = keywordList[iKeyword];
+ keywordMap[keyword] = true;
+ }
+
+ let tagArray = this._msgTagService.getAllTags({});
+ for (let iTag = 0; iTag < tagArray.length; iTag++) {
+ let tag = tagArray[iTag];
+ if (tag.key in keywordMap)
+ tags.push(tag);
+ }
+
+ yield Gloda.kWorkDone;
+ },
+};
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/fundattr.js
@@ -0,0 +1,544 @@
+/* ***** 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 Thunderbird Global Database.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Messaging, Inc.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Andrew Sutherland <asutherland@asutherland.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 ***** */
+
+EXPORTED_SYMBOLS = ['GlodaFundAttr'];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gloda/modules/log4moz.js");
+
+Cu.import("resource://gloda/modules/utils.js");
+Cu.import("resource://gloda/modules/gloda.js");
+Cu.import("resource://gloda/modules/datastore.js");
+
+Cu.import("resource://gloda/modules/noun_mimetype.js");
+
+
+/**
+ * @namespace The Gloda Fundamental Attribute provider is a special attribute
+ * provider; it provides attributes that the rest of the providers should be
+ * able to assume exist. Also, it may end up accessing things at a lower level
+ * than most extension providers should do. In summary, don't mimic this code
+ * unless you won't complain when your code breaks.
+ */
+var GlodaFundAttr = {
+ providerName: "gloda.fundattr",
+ _log: null,
+ _strBundle: null,
+
+ init: function gloda_explattr_init(aStrBundle) {
+ this._log = Log4Moz.Service.getLogger("gloda.fundattr");
+ this._strBundle = aStrBundle;
+
+ try {
+ this.defineAttributes();
+ }
+ catch (ex) {
+ this._log.error("Error in init: " + ex);
+ throw ex;
+ }
+ },
+
+ POPULARITY_FROM_ME_TO: 10,
+ POPULARITY_FROM_ME_CC: 4,
+ POPULARITY_TO_ME: 5,
+ POPULARITY_CC_ME: 1,
+
+ _attrConvSubject: null,
+ _attrFolder: null,
+ _attrBody: null,
+ _attrFrom: null,
+ _attrFromMe: null,
+ _attrTo: null,
+ _attrToMe: null,
+ _attrCc: null,
+ _attrCcMe: null,
+ _attrDate: null,
+
+ defineAttributes: function() {
+ /* ***** Conversations ***** */
+ // conversation: subjectMatches
+ this._attrConvSubject = Gloda.defineAttribute({
+ provider: this,
+ extensionName: Gloda.BUILT_IN,
+ attributeType: Gloda.kAttrDerived,
+ attributeName: "subjectMatches",
+ singular: true,
+ special: Gloda.kSpecialFulltext,
+ specialColumnName: "subject",
+ subjectNouns: [Gloda.NOUN_CONVERSATION],
+ objectNoun: Gloda.NOUN_FULLTEXT,
+ });
+
+ /* ***** Messages ***** */
+ // folder
+ this._attrFolder = Gloda.defineAttribute({
+ provider: this,
+ extensionName: Gloda.BUILT_IN,
+ attributeType: Gloda.kAttrFundamental,
+ attributeName: "folder",
+ singular: true,
+ special: Gloda.kSpecialColumn,
+ specialColumnName: "folderID",
+ subjectNouns: [Gloda.NOUN_MESSAGE],
+ objectNoun: Gloda.NOUN_FOLDER,
+ }); // tested-by: test_attributes_fundamental
+ this._attrFolder = Gloda.defineAttribute({
+ provider: this,
+ extensionName: Gloda.BUILT_IN,
+ attributeType: Gloda.kAttrFundamental,
+ attributeName: "messageKey",
+ singular: true,
+ special: Gloda.kSpecialColumn,
+ specialColumnName: "messageKey",
+ subjectNouns: [Gloda.NOUN_MESSAGE],
+ objectNoun: Gloda.NOUN_NUMBER,
+ }); // tested-by: test_attributes_fundamental
+
+ // bodyMatches. super-synthetic full-text matching...
+ this._attrBody = Gloda.defineAttribute({
+ provider: this,
+ extensionName: Gloda.BUILT_IN,
+ attributeType: Gloda.kAttrDerived,
+ attributeName: "bodyMatches",
+ singular: true,
+ special: Gloda.kSpecialFulltext,
+ specialColumnName: "body",
+ subjectNouns: [Gloda.NOUN_MESSAGE],
+ objectNoun: Gloda.NOUN_FULLTEXT,
+ }); // not-tested
+
+ // conversation
+ this._attrConversation = Gloda.defineAttribute({
+ provider: this,
+ extensionName: Gloda.BUILT_IN,
+ attributeType: Gloda.kAttrFundamental,
+ attributeName: "conversation",
+ singular: true,
+ special: Gloda.kSpecialColumnParent,
+ specialColumnName: "conversationID",
+ idStorageAttributeName: "_conversationID",
+ valueStorageAttributeName: "_conversation",
+ subjectNouns: [Gloda.NOUN_MESSAGE],
+ objectNoun: Gloda.NOUN_CONVERSATION,
+ });
+
+ // --- Fundamental
+ // From
+ this._attrFrom = Gloda.defineAttribute({
+ provider: this,
+ extensionName: Gloda.BUILT_IN,
+ attributeType: Gloda.kAttrFundamental,
+ attributeName: "from",
+ singular: true,
+ subjectNouns: [Gloda.NOUN_MESSAGE],
+ objectNoun: Gloda.NOUN_IDENTITY,
+ }); // tested-by: test_attributes_fundamental
+ // To
+ this._attrTo = Gloda.defineAttribute({
+ provider: this,
+ extensionName: Gloda.BUILT_IN,
+ attributeType: Gloda.kAttrFundamental,
+ attributeName: "to",
+ singular: false,
+ subjectNouns: [Gloda.NOUN_MESSAGE],
+ objectNoun: Gloda.NOUN_IDENTITY,
+ }); // tested-by: test_attributes_fundamental
+ // Cc
+ this._attrCc = Gloda.defineAttribute({
+ provider: this,
+ extensionName: Gloda.BUILT_IN,
+ attributeType: Gloda.kAttrFundamental,
+ attributeName: "cc",
+ singular: false,
+ subjectNouns: [Gloda.NOUN_MESSAGE],
+ objectNoun: Gloda.NOUN_IDENTITY,
+ }); // not-tested
+
+ // Date. now lives on the row.
+ this._attrDate = Gloda.defineAttribute({
+ provider: this,
+ extensionName: Gloda.BUILT_IN,
+ attributeType: Gloda.kAttrFundamental,
+ attributeName: "date",
+ singular: true,
+ special: Gloda.kSpecialColumn,
+ specialColumnName: "date",
+ subjectNouns: [Gloda.NOUN_MESSAGE],
+ objectNoun: Gloda.NOUN_DATE,
+ }); // tested-by: test_attributes_fundamental
+
+ // Attachment MIME Types
+ this._attrAttachmentTypes = Gloda.defineAttribute({
+ provider: this,
+ extensionName: Gloda.BUILT_IN,
+ attributeType: Gloda.kAttrFundamental,
+ attributeName: "attachmentTypes",
+ singular: false,
+ subjectNouns: [Gloda.NOUN_MESSAGE],
+ objectNoun: Gloda.NOUN_MIME_TYPE,
+ });
+
+ // --- Optimization
+ // Involves. Means any of from/to/cc. The queries get ugly enough without
+ // this that it seems to justify the cost, especially given the frequent
+ // use case. (In fact, post-filtering for the specific from/to/cc is
+ // probably justifiable rather than losing this attribute...)
+ this._attrInvolves = Gloda.defineAttribute({
+ provider: this,
+ extensionName: Gloda.BUILT_IN,
+ attributeType: Gloda.kAttrOptimization,
+ attributeName: "involves",
+ singular: false,
+ subjectNouns: [Gloda.NOUN_MESSAGE],
+ objectNoun: Gloda.NOUN_IDENTITY,
+ }); // not-tested
+
+ // From Me To
+ this._attrFromMeTo = Gloda.defineAttribute({
+ provider: this,
+ extensionName: Gloda.BUILT_IN,
+ attributeType: Gloda.kAttrOptimization,
+ attributeName: "fromMeTo",
+ singular: false,
+ subjectNouns: [Gloda.NOUN_MESSAGE],
+ objectNoun: Gloda.NOUN_PARAM_IDENTITY,
+ }); // not-tested
+ // From Me Cc
+ this._attrFromMeCc = Gloda.defineAttribute({
+ provider: this,
+ extensionName: Gloda.BUILT_IN,
+ attributeType: Gloda.kAttrOptimization,
+ attributeName: "fromMeCc",
+ singular: false,
+ subjectNouns: [Gloda.NOUN_MESSAGE],
+ objectNoun: Gloda.NOUN_PARAM_IDENTITY,
+ }); // not-tested
+ // To Me
+ this._attrToMe = Gloda.defineAttribute({
+ provider: this,
+ extensionName: Gloda.BUILT_IN,
+ attributeType: Gloda.kAttrFundamental,
+ attributeName: "toMe",
+ singular: false,
+ subjectNouns: [Gloda.NOUN_MESSAGE],
+ objectNoun: Gloda.NOUN_PARAM_IDENTITY,
+ }); // not-tested
+ // Cc Me
+ this._attrCcMe = Gloda.defineAttribute({
+ provider: this,
+ extensionName: Gloda.BUILT_IN,
+ attributeType: Gloda.kAttrFundamental,
+ attributeName: "ccMe",
+ singular: false,
+ subjectNouns: [Gloda.NOUN_MESSAGE],
+ objectNoun: Gloda.NOUN_PARAM_IDENTITY,
+ }); // not-tested
+
+
+ // -- Mailing List
+ // Non-singular, but a hard call. Namely, it is obvious that a message can
+ // be addressed to multiple mailing lists. However, I don't see how you
+ // could receive a message with more than one set of List-* headers,
+ // since each list-serve would each send you a copy. Based on our current
+ // decision to treat each physical message as separate, it almost seems
+ // right to limit the list attribute to the copy that originated at the
+ // list. That may sound entirely wrong, but keep in mind that until we
+ // have seen a message from the list with the List headers, we can't
+ // definitely know it's a mailing list (although heuristics could take us
+ // pretty far). As such, the quasi-singular thing is appealing.
+ // Of course, the reality is that we really want to know if a message was
+ // sent to multiple mailing lists and be able to query on that.
+ // Additionally, our implicit-to logic needs to work on messages that
+ // weren't relayed by the list-serve, especially messages sent to the list
+ // by the user.
+ this._attrList = Gloda.defineAttribute({
+ provider: this,
+ extensionName: Gloda.BUILT_IN,
+ attributeType: Gloda.kAttrFundamental,
+ attributeName: "mailing-list",
+ bindName: "mailingLists",
+ singular: false,
+ subjectNouns: [Gloda.NOUN_MESSAGE],
+ objectNoun: Gloda.NOUN_IDENTITY,
+ }); // not-tested, not-implemented
+ },
+
+ /**
+ *
+ * Specializations:
+ * - Mailing Lists. Replies to a message on a mailing list frequently only
+ * have the list-serve as the 'to', so we try to generate a synthetic 'to'
+ * based on the author of the parent message when possible. (The 'possible'
+ * part is that we may not have a copy of the parent message at the time of
+ * processing.)
+ * - Newsgroups. Same deal as mailing lists.
+ */
+ process: function gloda_fundattr_process(aGlodaMessage, aRawReps,
+ aIsNew, aCallbackHandle) {
+ let aMsgHdr = aRawReps.header;
+ let aMimeMsg = aRawReps.mime;
+
+ // -- From
+ // Let's use replyTo if available.
+ // er, since we are just dealing with mailing lists for now, forget the
+ // reply-to...
+ // TODO: deal with default charset issues
+ let author = null;
+ /*
+ try {
+ author = aMsgHdr.getStringProperty("replyTo");
+ }
+ catch (ex) {
+ }
+ */
+ if (author == null || author == "")
+ author = aMsgHdr.author;
+
+ let [authorIdentities, toIdentities, ccIdentities] =
+ yield aCallbackHandle.pushAndGo(
+ Gloda.getOrCreateMailIdentities(aCallbackHandle,
+ author, aMsgHdr.recipients,
+ aMsgHdr.ccList));
+
+ if (authorIdentities.length == 0) {
+ this._log.error("Message with subject '" + aMsgHdr.mime2DecodedSubject +
+ "' somehow lacks a valid author. Bailing.");
+ return; // being a generator, this generates an exception; we like.
+ }
+ let authorIdentity = authorIdentities[0];
+ aGlodaMessage.from = authorIdentity;
+
+ // -- To, Cc
+ aGlodaMessage.to = toIdentities;
+ aGlodaMessage.cc = ccIdentities;
+
+ // -- Attachments
+ let attachmentTypes = [];
+ for each (let attachment in aMimeMsg.allAttachments) {
+ if (attachment.isRealAttachment) {
+ attachmentTypes.push(MimeTypeNoun.getMimeType(attachment.contentType));
+ }
+ }
+ if (attachmentTypes.length) {
+ aGlodaMessage.attachmentTypes = attachmentTypes;
+ }
+
+ // TODO: deal with mailing lists, including implicit-to. this will require
+ // convincing the indexer to pass us in the previous message if it is
+ // available. (which we'll simply pass to everyone... it can help body
+ // logic for quoting purposes, etc. too.)
+
+ yield Gloda.kWorkDone;
+ },
+
+ optimize: function gloda_fundattr_process(aGlodaMessage, aRawReps,
+ aIsNew, aCallbackHandle) {
+
+ let involvesIdentities = {};
+ let involves = aGlodaMessage.involves || [];
+
+ // me specialization optimizations
+ let toMe = aGlodaMessage.toMe || [];
+ let fromMeTo = aGlodaMessage.fromMeTo || [];
+ let ccMe = aGlodaMessage.ccMe || [];
+ let fromMeCc = aGlodaMessage.fromMeCc || [];
+
+ let myIdentities = Gloda.myIdentities; // needless optimization?
+ let authorIdentity = aGlodaMessage.from;
+ let isFromMe = authorIdentity.id in myIdentities;
+
+ involves.push(authorIdentity);
+ involvesIdentities[authorIdentity.id] = true;
+
+ for each (let [,toIdentity] in Iterator(aGlodaMessage.to)) {
+ if (!(toIdentity.id in involvesIdentities)) {
+ involves.push(toIdentity);
+ involvesIdentities[toIdentity.id] = true;
+ }
+
+ // optimization attribute to-me ('I' am the parameter)
+ if (toIdentity.id in myIdentities) {
+ toMe.push([toIdentity, authorIdentity]);
+ if (aIsNew)
+ authorIdentity.contact.popularity += this.POPULARITY_TO_ME;
+ }
+ // optimization attribute from-me-to ('I' am the parameter)
+ if (isFromMe) {
+ fromMeTo.push([authorIdentity, toIdentity]);
+ // also, popularity
+ if (aIsNew)
+ toIdentity.contact.popularity += this.POPULARITY_FROM_ME_TO;
+ }
+ }
+ for each (let [,ccIdentity] in Iterator(aGlodaMessage.cc)) {
+ if (!(ccIdentity.id in involvesIdentities)) {
+ involves.push(ccIdentity);
+ involvesIdentities[ccIdentity.id] = true;
+ }
+ // optimization attribute cc-me ('I' am the parameter)
+ if (ccIdentity.id in myIdentities) {
+ ccMe.push([ccIdentity, authorIdentity]);
+ if (aIsNew)
+ authorIdentity.contact.popularity += this.POPULARITY_CC_ME;
+ }
+ // optimization attribute from-me-to ('I' am the parameter)
+ if (isFromMe) {
+ fromMeCc.push([authorIdentity, ccIdentity]);
+ // also, popularity
+ if (aIsNew)
+ ccIdentity.contact.popularity += this.POPULARITY_FROM_ME_CC;
+ }
+ }
+
+ aGlodaMessage.involves = involves;
+ if (toMe.length)
+ aGlodaMessage.toMe = toMe;
+ if (fromMeTo.length)
+ aGlodaMessage.fromMeTo = fromMeTo;
+ if (ccMe.length)
+ aGlodaMessage.ccMe = ccMe;
+ if (fromMeCc.length)
+ aGlodaMessage.fromMeCc = fromMeCc;
+
+ if (aRawReps.bodyLines &&
+ this.contentWhittle(aGlodaMessage, {}, aRawReps.bodyLines,
+ aRawReps.content)) {
+ // we were going to do something here?
+ }
+
+ yield Gloda.kWorkDone;
+ },
+
+ _countQuoteDepthAndNormalize:
+ function gloda_fundattr__countQuoteDepthAndNormalize(aLine) {
+ let count = 0;
+ let lastStartOffset = 0;
+
+ for (let i = 0; i < aLine.length; i++) {
+ let c = aLine[i];
+ if (c == ">") {
+ count++;
+ lastStartOffset = i+1;
+ }
+ else if (c == " ") {
+ }
+ else {
+ return [count,
+ lastStartOffset ? aLine.substring(lastStartOffset) : aLine];
+ }
+ }
+
+ return [count, lastStartOffset ? aLine.substring(lastStartOffset) : aLine];
+ },
+
+ /**
+ * Attempt to understand simple quoting constructs that use ">" with
+ * obvious phrases to enter the quoting block. No support for other types
+ * of quoting at this time. Also no support for piercing the wrapper of
+ * forwarded messages to actually be the content of the forwarded message.
+ */
+ contentWhittle: function gloda_fundattr_contentWhittle(aGlodaMessage,
+ aMeta, aBodyLines, aContent) {
+ if (!aContent.volunteerContent(aContent.kPriorityBase))
+ return false;
+
+ // duplicate the list; we mutate somewhat...
+ let bodyLines = aBodyLines.concat();
+
+ let rangeStart = 0, lastNonBlankLine = null;
+ let inQuoteDepth = 0;
+ for each (let [iLine, line] in Iterator(bodyLines)) {
+ if (!line)
+ continue;
+
+ if (line[0] == ">") {
+ if (!inQuoteDepth) {
+ let rangeEnd = iLine - 1;
+ let quoteRangeStart = iLine;
+ // see if the last non-blank-line was a lead-in...
+ if (lastNonBlankLine != null) {
+ if (aBodyLines[lastNonBlankLine].indexOf("wrote") >= 0) {
+ quoteRangeStart = lastNonBlankLine;
+ rangeEnd = lastNonBlankLine - 1;
+ }
+ }
+ if (rangeEnd >= rangeStart)
+ aContent.content(aBodyLines.slice(rangeStart, rangeEnd+1));
+
+ [inQuoteDepth, line] = this._countQuoteDepthAndNormalize(line);
+ bodyLines[iLine] = line;
+ rangeStart = quoteRangeStart;
+ }
+ else {
+ let curQuoteDepth;
+ [curQuoteDepth, line] = this._countQuoteDepthAndNormalize(line);
+ bodyLines[iLine] = line;
+
+ if (curQuoteDepth != inQuoteDepth) {
+ // we could do some "wrote" compensation here, but it's not really
+ // as important. let's wait for a more clever algorithm.
+ aContent.quoted(aBodyLines.slice(rangeStart, iLine), inQuoteDepth);
+ inQuoteDepth = curQuoteDepth;
+ rangeStart = iLine;
+ }
+ }
+ }
+ else {
+ if (inQuoteDepth) {
+ aContent.quoted(aBodyLines.slice(rangeStart, iLine), inQuoteDepth);
+ inQuoteDepth = 0;
+ rangeStart = iLine;
+ }
+ }
+
+ lastNonBlankLine = iLine;
+ }
+
+ if (inQuoteDepth) {
+ aContent.quoted(aBodyLines.slice(rangeStart), inQuoteDepth);
+ }
+ else {
+ aContent.content(aBodyLines.slice(rangeStart));
+ }
+
+ return true;
+ },
+};
new file mode 100644
--- /dev/null
+++ b/mailnews/db/gloda/modules/gloda.js
@@ -0,0 +1,1643 @@
+/* ***** 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 Thunderbird Global Database.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Messaging, Inc.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Andrew Sutherland <asutherland@asutherland.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 ***** */
+
+EXPORTED_SYMBOLS = ['Gloda'];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gloda/modules/log4moz.js");
+
+Cu.import("resource://gloda/modules/datastore.js");
+Cu.import("resource://gloda/modules/datamodel.js");
+Cu.import("resource://gloda/modules/collection.js");
+Cu.import("resource://gloda/modules/connotent.js");
+Cu.import("resource://gloda/modules/query.js");
+Cu.import("resource://gloda/modules/utils.js");
+
+/**
+ * Provides the user-visible (and extension visible) global database
+ * functionality. There is currently a dependency/ordering
+ * problem in that the concept of 'gloda' also includes some logic that is
+ * contributed by built-in extensions, if you will. Those built-in extensions
+ * (fundattr.js, explattr.js) also import this file. To avoid a circular
+ * dependency, those built-in extensions are loaded by everybody.js. The
+ * simplest/best solution is probably to move everybody.js to be gloda.js and
+ * have it re-export only 'Gloda'. gloda.js (this file) can then move to be
+ * gloda_int.js (or whatever our eventual naming scheme is), which built-in
+ * extensions can explicitly rely upon.
+ *
+ * === Concepts
+ *
+ * == Nouns
+ *
+ * Inspired by reasonable uses of triple-stores, I have tried to leverage
+ * existing model and terminology rather than rolling out own for everything.
+ * The idea with triple-stores is that you have a subject, a predicate, and an
+ * object. For example, if we are talking about a message, that is the
+ * subject, the predicate could roughly be sent-by, and the object a person.
+ * We can generalize this idea to say that the subject and objects are nouns.
+ * Since we want to be more flexible than only dealing with messages, we
+ * therefore introduce the concept of nouns as an organizing principle.
+ *
+ * == Attributes
+ *
+ * Our attributes definitions are basically our predicates. When we define
+ * an attribute, it's a label with a bunch of meta-data. Our attribute
+ * instances are basically a 'triple' in a triple-store. The attributes
+ * are stored in database rows that imply a specific noun-type (ex: the
+ * messageAttributes table), with an ID identifying the message which is our
+ * subject, an attribute ID which identifies the attribute definition in use
+ * (and therefore the predicate), plus an object ID (given context aka the
+ * noun type by the attribute's meta-data) which identifies the 'object'.
+ *
+ * == But...
+ *
+ * Things aren't entirely as clear as they could be right now, terminology/
+ * concept/implementation-wise. Some work is probably still in order.
+ *
+ * === Implementation
+ *
+ * == Nouns
+ *
+ * So, we go and define the nouns that are roughly the classes in our data
+ * model. Every 'class' we define in datamodel.js is a noun that gets defined
+ * here in the Gloda core. We provide sufficient meta-data about the noun to
+ * serialize/deserialize its representation from our database representation.
+ * Nouns do not have to be defined in this class, but can also be contributed
+ * by external code.
+ * We have a concept of 'first class' nouns versus non-first class nouns. The
+ * distinction is meant to be whether we can store meta-information about those
+ * nouns using attributes. Right now, only message are real first-class nouns,
+ * but we want to expand that to include contacts and eventually events and
+ * tasks as lightning-integration occurs. In practice, we are stretching the
+ * definition of first-class nouns slightly to include things we can't store
+ * meta-data about, but want to be able to query about. We do want to resolve
+ * this.
+ *
+ * == Attributes
+ *
+ * Attributes are defined by "attribute providers" who are responsible for
+ * taking an instance of a first-class noun (for which they are registered)
+ * plus perhaps some other meta-data, and returning a list of attributes
+ * extracted from that noun. For now, this means messages. Attribute
+ * providers may create new data records as a side-effect of the indexing
+ * process, although we have not yet fully dealt with the problem of deleting
+ * these records should they become orphaned in the database due to the
+ * purging of a message and its attributes.
+ * All of the 'core' gloda attributes are provided by the fundattr.js and
+ * explattr.js providers.
+ *
+ * === (Notable) Future Work
+ *
+ * == Attributes
+ *
+ * Attribute mechanisms currently lack any support for 'overriding' attributes
+ * provided by other attribute providers. For example, the fundattr provider
+ * tells us who a message is 'from' based on the e-mail address present.
+ * However, other plugins may actually know better. For example, the bugzilla
+ * daemon e-mails based on bug activity although the daemon gets the credit
+ * as the official sender. A bugzilla plugin can easily extract the actual
+ * person/e-mail addressed who did something on the bug to cause the
+ * notification to be sent. In practice, we would like that person to be
+ * the 'sender' of the bugmail. But we can't really do that right, yet.
+ *
+ * @namespace
+ */
+var Gloda = {
+ /**
+ * Initialize logging, the datastore (SQLite database), the core nouns and
+ * attributes, and the contact and identities that belong to the presumed
+ * current user (based on accounts).
+ *
+ * Additional nouns and the core attribute providers are initialized by the
+ * everybody.js module which ensures all of those dependencies are loaded
+ * (and initialized).
+ */
+ _init: function gloda_ns_init() {
+ this._initLogging();
+ this._json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
+ GlodaDatastore._init(this._json, this._nounIDToDef);
+ this._initAttributes();
+ this._initMyIdentities();
+ },
+
+ _log: null,
+ /**
+ * Initialize logging; the error console window gets Warning/Error, and stdout
+ * (via dump) gets everything.
+ */
+ _initLogging: function gloda_ns_initLogging() {
+ let formatter = new Log4Moz.BasicFormatter();
+ let root = Log4Moz.Service.rootLogger;
+ root.level = Log4Moz.Level.Debug;
+
+ let enableConsoleLogging = false;
+ let enableDumpLogging = false;
+ let considerNetLogging = false;
+
+ try {
+ // figure out if event-driven indexing should be enabled...
+ let prefService = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefService);
+ let branch = prefService.getBranch("mailnews.database.global.logging.");
+ enableConsoleLogging = branch.getBoolPref("console");
+ enableDumpLogging = branch.getBoolPref("dump");
+ enableNetLogging = branch.getBoolPref("net");
+ } catch (ex) {}
+
+ if (enableConsoleLogging) {
+ let capp = new Log4Moz.ConsoleAppender(formatter);
+ capp.level = Log4Moz.Level.Warn;
+ root.addAppender(capp);
+ }
+
+ if (enableDumpLogging) {
+ let dapp = new Log4Moz.DumpAppender(formatter);
+ dapp.level = Log4Moz.Level.All;
+ root.addAppender(dapp);
+ }
+
+ if (considerNetLogging) {
+ let file = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("TmpD", Ci.nsIFile);
+ file.append("chainsaw.ptr");
+ if (file.exists()) {
+ let data = GlodaUtils.loadFileToString(file);
+ data = data.trim();
+ let [host, port] = data.split(":");
+ let xf = new Log4Moz.XMLFormatter();
+ let sapp = new Log4Moz.SocketAppender(host, Number(port), xf);
+ sapp.level = Log4Moz.Level.All;
+ root.addAppender(sapp);
+ }
+ }
+
+ this._log = Log4Moz.Service.getLogger("gloda.NS");
+ this._log.info("Logging Initialized");
+ },
+
+ kIndexerIdle: 0,
+ kIndexerIndexing: 1,
+ kIndexerMoving: 2,
+ kIndexerRemoving: 3,
+
+ /** Synchronous activities performed, you can drive us more. */
+ kWorkSync: 0,
+ /**
+ * Asynchronous activity performed, you need to relinquish flow control and
+ * trust us to call callbackDriver later.
+ */
+ kWorkAsync: 1,
+ /**
+ * We are all done with our task, close us and figure out something else to do.
+ */
+ kWorkDone: 2,
+ /**
+ * We are not done with our task, but we think it's a good idea to take a
+ * breather.
+ */
+ kWorkPause: 3,
+ /**
+ * We are done with our task, and have a result that we are returning. This
+ * should only be used by your callback handler's doneWithResult method.
+ * Ex: you are passed aCallbackHandle, and you do
+ * "yield aCallbackHandle.doneWithResult(myResult);".
+ */
+ kWorkDoneWithResult: 4,
+
+ /**
+ * Lookup a gloda message from an nsIMsgDBHdr.
+ *
+ * @param aMsgHdr The header of the message you want the gloda message for.
+ *
+ * @return the gloda messages that corresponds to the provided nsIMsgDBHdr
+ * if one exists, null if one cannot be found.
+ */
+ getMessageCollectionForHeader: function gloda_ns_getMessageForHeader(aMsgHdr,
+ aListener, aData) {
+ let query = Gloda.newQuery(Gloda.NOUN_MESSAGE);
+ query.folder(aMsgHdr.folder).messageKey(aMsgHdr.messageKey);
+ return query.getCollection(aListener, aData);
+ },
+
+ getMessageContent: function gloda_ns_getMessageContent(aGlodaMessage,
+ aMimeMsg) {
+ let content = new GlodaContent();
+
+ let meta = {subject: aMimeMsg.get("subject")};
+
+ let bodyLines = aMimeMsg.bodyPlain.split(/\r?\n/);
+
+ // get the provider list in reverse order, mainly because we want more
+ // specific content processors to get a chance before the default one
+ let attributeProviders =
+ this._attrProviderOrderByNoun[this.NOUN_MESSAGE].concat().reverse();
+ for each (let [, attrProvider] in Iterator(attributeProviders)) {
+ if (attrProvider.contentWhittle) {
+ try {
+ attrProvider.contentWhittle(aGlodaMessage, meta, bodyLines, content);
+ }
+ catch (ex) {
+ this._log.warn("Content whittler exception " + ex.fileName + ":" +
+ ex.lineNumber + ": " + ex);
+ }
+ }
+ }
+
+ return content;
+ },
+
+ getFolderForFolder: function gloda_ns_getFolderForFolder(aMsgFolder) {
+ return GlodaDatastore._mapFolder(aMsgFolder);
+ },
+
+ /**
+ * Given one or more full mail addresses (ex: "Bob Smith" <bob@smith.com>),
+ * return a list of the identities that corresponds to each mail address,
+ * creating them as required.
+ */
+ getOrCreateMailIdentities:
+ function gloda_ns_getOrCreateMailIdentities(aCallbackHandle) {
+ let addresses = {};
+ let resultLists = [];
+
+ for (let iArg = 1; iArg < arguments.length; iArg++) {
+ let aMailAddresses = arguments[iArg];
+ let parsed = GlodaUtils.parseMailAddresses(aMailAddresses);
+
+ let resultList = [];
+ resultLists.push(resultList);
+
+ let identities = [];
+ for (let iAddress = 0; iAddress < parsed.count; iAddress++) {
+ let address = parsed.addresses[iAddress];
+ if (address in addresses)
+ addresses[address].push(resultList);
+ else
+ addresses[address] = [parsed.names[iAddress], resultList];
+ }
+ }
+
+ let query = this.newQuery(this.NOUN_IDENTITY);
+ query.kind("email");
+ query.value.apply(query, [address for (address in addresses)]);
+ let collection = query.getCollection(aCallbackHandle);
+ yield this.kWorkAsync;
+
+ // put the identities in the appropriate result lists
+ for each (let [, identity] in Iterator(collection.items)) {
+ let nameAndResultLists = addresses[identity.value];
+ this._log.debug(" found identity for '" + nameAndResultLists[0] + "' (" +
+ identity.value + ")");
+ // index 0 is the name, skip it
+ for (let iResList = 1; iResList < nameAndResultLists.length; iResList++) {
+ nameAndResultLists[iResList].push(identity);
+ }
+ delete addresses[identity.value];
+ }
+
+ // create the identities that did not exist yet
+ for each (let [address, nameAndResultLists] in Iterator(addresses)) {
+ let name = nameAndResultLists[0];
+
+ this._log.debug(" creating contact for '" + name + "' (" + address + ")");
+
+ // try and find an existing address book contact.
+ let card = GlodaUtils.getCardForEmail(address);
+ // XXX when we have the address book GUID stuff, we need to use that to
+ // find existing contacts... (this will introduce a new query phase
+ // where we batch all the GUIDs for an async query)
+ // XXX when the address book supports multiple e-mail addresses, we
+ // should also just create identities for any that don't yet exist
+
+ // if there is no name, just use the e-mail (the ab indexer actually
+ // processes the card's displayName for synchronization, so we don't
+ // need to do that.)
+ if (!name)
+ name = address;
+
+ let contact = GlodaDatastore.createContact(null, null, name, 0, 0);
+
+ // we must create the identity. use a blank description because there's
+ // nothing to differentiate it from other identities, as this contact
+ // only has one initially (us).
+ // XXX when we have multiple e-mails and there is a meaning associated
+ // with each e-mail, try and use that to populate the description.
+ // XXX we are creating the identity here before we insert the contact.
+ // conceptually it is good for us to be creating the identity before
+ // exposing it to the address-book indexer, but we could get our id's
+ // in a bad way from not deferring the identity insertion until after
+ // the contact insertion.
+ let identity = GlodaDatastore.createIdentity(contact.id, contact,
+ "email", address, /* description */ "", /* relay? */ false);
+ contact._identities = [identity];
+
+ // give the address book indexer a chance if we have a card.
+ // (it will fix-up the name based on the card as appropriate)
+ if (card)
+ yield aCallbackHandle.pushAndGo(
+ Gloda.grokNounItem(contact, card, true, aCallbackHandle));
+ else // grokNounItem will issue the insert for us...
+ GlodaDatastore.insertContact(contact);
+
+ for (let iResList = 1; iResList < nameAndResultLists.length; iResList++) {
+ nameAndResultLists[iResList].push(identity);
+ }
+ }
+
+ yield aCallbackHandle.doneWithResult(resultLists);
+ },
+
+ /**
+ * Dictionary of the user's known identities; key is the identity id, value
+ * is the actual identity. This is populated by _initMyIdentities based on
+ * the accounts defined.
+ */
+ myIdentities: {},
+ /**
+ * The contact corresponding to the current user. We are assuming that only
+ * a single user/human being uses the current profile. This is known to be
+ * a flawed assumption, but is the best first approximation available.
+ *
+ * @TODO attempt to deal with multile people using the same profile
+ */
+ myContact: null,
+ /**
+ * Populate myIdentities with all of our identities. Currently we do this
+ * by assuming that there is one human/user per profile, and that all of the
+ * accounts defined in the profile belong to them. The single contact is
+ * stored on myContact.
+ *
+ * @TODO deal with account addition/modification/removal
+ * @TODO attempt to deal with multiple people using the same profile
+ */
+ _initMyIdentities: function gloda_ns_initMyIdentities() {
+ let myContact = null;
+ let myIdentities = {};
+ let myEmailAddresses = {}; // process each email at most once; stored here
+
+ let fullName = null;
+ let existingIdentities = [];
+ let identitiesToCreate = [];
+
+ let msgAccountManager = Cc["@mozilla.org/messenger/account-manager;1"].
+ getService(Ci.nsIMsgAccountManager);
+ let numIdentities = msgAccountManager.allIdentities.Count();
+
+ // nothing to do if there are no accounts/identities.
+ if (!numIdentities)
+ return;
+
+ for (let iIdentity = 0; iIdentity < numIdentities; iIdentity++) {
+ let msgIdentity = msgAccountManager.allIdentities.GetElementAt(iIdentity)
+ .QueryInterface(Ci.nsIMsgIdentity);
+
+ if (fullName === null)
+ fullName = msgIdentity.fullName;
+
+ let emailAddress = msgIdentity.email;
+ let replyTo = msgIdentity.replyTo;
+
+ // find the identities if they exist, flag to create them if they don't
+ if (emailAddress) {
+ parsed = GlodaUtils.parseMailAddresses(emailAddress);
+ if (!(parsed.addresses[0] in myEmailAddresses)) {
+ let identity = GlodaDatastore.getIdentity("email",
+ parsed.addresses[0]);
+ if (identity)
+ existingIdentities.push(identity);
+ else
+ identitiesToCreate.push(parsed.addresses[0]);
+ myEmailAddresses[parsed.addresses[0]] = true;
+ }
+ }
+ if (replyTo) {
+ parsed = GlodaUtils.parseMailAddresses(replyTo);
+ if (!(parsed.addresses[0] in myEmailAddresses)) {
+ let identity = GlodaDatastore.getIdentity("email",
+ parsed.addresses[0]);
+ if (identity)
+ existingIdentities.push(identity);
+ else
+ identitiesToCreate.push(parsed.addresses[0]);
+ myEmailAddresses[parsed.addresses[0]] = true;
+ }
+ }
+ }
+
+ // we need to establish the identity.contact portions of the relationship
+ for each (let [,identity] in Iterator(existingIdentities)) {
+ identity._contact = GlodaDatastore.getContactByID(identity.contactID);
+ }
+
+ if (existingIdentities.length) {
+ // just use the first guy's contact
+ myContact = existingIdentities[0].contact;
+ }
+ else {
+ // create a new contact
+ myContact = GlodaDatastore.createContact(null, null, fullName || "Me",
+ 0, 0);
+ GlodaDatastore.insertContact(myContact);
+ }
+
+ if (identitiesToCreate.length) {
+ for (let iIdentity = 0; iIdentity < identitiesToCreate.length;
+ iIdentity++) {
+ let emailAddress = identitiesToCreate[iIdentity];
+ // XXX this won't always be of type "email" as we add new account types
+ // XXX the blank string could be trying to differentiate; we do have
+ // enough info to do it.
+ let identity = GlodaDatastore.createIdentity(myContact.id, myContact,
+ "email",
+ emailAddress,
+ "", false);
+ existingIdentities.push(identity);
+ }
+ }
+
+ for (let iIdentity = 0; iIdentity < existingIdentities.length;
+ iIdentity++) {
+ let identity = existingIdentities[iIdentity];
+ myIdentities[identity.id] = identity;
+ }
+
+ this.myContact = myContact;
+ this.myIdentities = myContact._identities = myIdentities;
+ },
+
+ /**
+ * An attribute that is a defining characteristic of the subject.
+ */
+ kAttrFundamental: 0,
+ /**
+ * An attribute that is an optimization derived from two or more fundamental
+ * attributes and exists solely to improve database query performance.
+ */
+ kAttrOptimization: 1,
+ /**
+ * An attribute that is derived from the content of the subject. For example,
+ * a message that references a bugzilla bug could have a "derived" attribute
+ * that captures the bugzilla reference. This is not
+ */
+ kAttrDerived: 2,
+ /**
+ * An attribute that is the result of an explicit and intentional user action
+ * upon the subject. For example, a tag placed on a message by a user (or
+ * at the user's request by a filter) is explicit.
+ */
+ kAttrExplicit: 3,
+ /**
+ * An attribute that is indirectly the result of a user's behaviour. For
+ * example, if a user consults a message multiple times, we may conclude that
+ * the user finds the message interesting. It is "implied", if you will,
+ * that the message is interesting.
+ */
+ kAttrImplicit: 4,
+
+ /**
+ * This attribute is not 'special'; it is stored as a (thing id, attribute id,
+ * attribute id) tuple in the database rather than on thing's row or on
+ * thing's fulltext row. (Where "thing" could be a message or any other
+ * first class noun.)
+ */
+ kSpecialNotAtAll: GlodaDatastore.kSpecialNotAtAll,
+ /**
+ * This attribute is stored as a numeric column on the row for the noun. The
+ * attribute definition should include this value as 'special' and the
+ * column name that stores the attribute as 'specialColumnName'.
+ */
+ kSpecialColumn: GlodaDatastore.kSpecialColumn,
+ kSpecialColumnChildren: GlodaDatastore.kSpecialColumnChildren,
+ kSpecialColumnParent: GlodaDatastore.kSpecialColumnParent,
+ /**
+ * This attribute is stored as a string column on the row for the noun. It
+ * differs from kSpecialColumn in that it is a string and thus uses different
+ * query mechanisms.
+ */
+ kSpecialString: GlodaDatastore.kSpecialString,
+ /**
+ * This attribute is stored as a fulltext column on the fulltext table for
+ * the noun. The attribute defintion should include this value as 'special'
+ * and the column name that stores the table as 'specialColumnName'.
+ */
+ kSpecialFulltext: GlodaDatastore.kSpecialFulltext,
+
+ /**
+ * The extensionName used for the attributes defined by core gloda plugins
+ * such as fundattr.js and explattr.js.
+ */
+ BUILT_IN: "built-in",
+
+
+ /*
+ * The following are explicit noun IDs. While most extension-provided nouns
+ * will have dynamically allocated id's that are looked up by name, these
+ * id's can be relied upon to exist and be accessible via these
+ * pseudo-constants. It's not really clear that we need these, although it
+ * does potentially simplify code to not have to look up all of their nouns
+ * at initialization time.
+ */
+ /**
+ * Boolean values, expressed as 0/1 in the database and non-continuous for
+ * constraint purposes. Like numbers, such nouns require their attributes
+ * to provide them with context, lacking any of their own.
+ * Having this as a noun type may be a bad idea; a change of nomenclature
+ * (so that we are not claiming a boolean value is a noun, but still using
+ * it in the same way) or implementation to require each boolean noun
+ * actually be its own noun may be in order.
+ */
+ NOUN_BOOLEAN: 1,
+ /**
+ * A number, which could mean an integer or floating point values. We treat
+ * these as continuous, meaning that queries on them can have ranged
+ * constraints expressed on them. Lacking any inherent context, numbers
+ * depend on their attributes to parameterize them as required.
+ * Same deal as with NOUN_BOOLEAN, we may need to change this up conceptually.
+ */
+ NOUN_NUMBER: 2,
+ /**
+ * A (non-fulltext) string.
+ * Same deal as with NOUN_BOOLEAN, we may need to change this up conceptually.
+ */
+ NOUN_STRING: 3,
+ /** A date, encoded as a PRTime, represented as a js Date object. */
+ NOUN_DATE: 10,
+ /**
+ * Fulltext search support, somewhat magical. This is only intended to be
+ * used for kSpecialFulltext attributes, and exclusively as a constraint
+ * mechanism. The values are always represented as strings. It is presumed
+ * that the user of this functionality knows how to generate SQLite FTS3
+ * style MATCH queries, or is okay with us just gluing them together with
+ * " OR " when used in an or-constraint case. Gloda's query mechanism
+ * currently lacks the ability to to compile Gloda-style and-constraints
+ * into a single MATCH query, but it will turn out okay, just less
+ * efficiently than it could.
+ */
+ NOUN_FULLTEXT: 20,
+ /**
+ * Represents a MIME Type. We currently lack any human-intelligible
+ * descriptions of mime types.
+ */
+ NOUN_MIME_TYPE: 40,
+ /**
+ * Captures a message tag as well as when the tag's presence was observed,
+ * hoping to approximate when the tag was applied. It's a somewhat dubious
+ * attempt to not waste our opporunity to store a value along with the tag.
+ * (The tag is actually stored as an attribute parameter on the attribute
+ * definition, rather than a value in the attribute 'instance' for the
+ * message.)
+ */
+ NOUN_TAG: 50,
+ /**
+ * Doesn't actually work owing to a lack of an object to represent a folder.
+ * We do expose the folderURI and folderID of a message, but need to map that
+ * to a good abstraction. Probably something thin around a SteelFolder or
+ * the like; we would contribute the functionality to easily move from a
+ * folder to the list of gloda messages in that folder, as well as the
+ * indexing preferences for that folder.
+ * @TODO folder noun and related abstraction
+ */
+ NOUN_FOLDER: GlodaFolder.prototype.NOUN_ID, // 100
+ /**
+ * All messages belong to a conversation. See datamodel.js for the
+ * definition of the GlodaConversation class.
+ */
+ NOUN_CONVERSATION: GlodaConversation.prototype.NOUN_ID, // 101
+ /**
+ * A one-to-one correspondence with underlying (indexed) nsIMsgDBHdr
+ * instances. See datamodel.js for the definition of the GlodaMessage class.
+ */
+ NOUN_MESSAGE: GlodaMessage.prototype.NOUN_ID, // 102
+ /**
+ * Corresponds to a human being, who may have multiple electronic identities
+ * (a la NOUN_IDENTITY). There is no requirement for association with an
+ * address book contact, although when the address book contact exists,
+ * we want to be associated with it. See datamodel.js for the definition
+ * of the GlodaContact class.
+ */
+ NOUN_CONTACT: GlodaContact.prototype.NOUN_ID, // 103
+ /**
+ * A single identity of a contact, who may have one or more. E-mail accounts,
+ * instant messaging accounts, social network site accounts, etc. are each
+ * identities. See datamodel.js for the definition of the GlodaIdentity
+ * class.
+ */
+ NOUN_IDENTITY: GlodaIdentity.prototype.NOUN_ID, // 104
+
+ /**
+ * Parameterized identities, for use in the from-me, to-me, cc-me optimization
+ * cases. Not for reuse without some thought. These nouns use the parameter
+ * to store the 'me' identity that we are talking about, and the value to
+ * store the identity of the other party. So in both the from-me and to-me
+ * cases involving 'me' and 'foo@bar', the 'me' identity is always stored via
+ * the attribute parameter, and the 'foo@bar' identity is always stored as
+ * the attribute value. See fundattr.js for more information on this, but
+ * you probably shouldn't be touching this unless you are fundattr.
+ */
+ NOUN_PARAM_IDENTITY: 200,
+
+ /** Next Noun ID to hand out, these don't need to be persisted (for now). */
+ _nextNounID: 1000,
+
+ /**
+ * Maps noun names to noun IDs.
+ */
+ _nounNameToNounID: {},
+ /**
+ * Maps noun IDs to noun definition dictionaries. (Noun definition
+ * dictionaries provided to us at the time a noun was defined, plus some
+ * additional stuff we put in there.)
+ */
+ _nounIDToDef: {},
+
+ _managedToJSON: function gloda_ns_managedToJSON(aItem) {
+ return aItem.id;
+ },
+
+ /**
+ * Define a noun. Takes a dictionary with the following keys/values:
+ *
+ * @param name The name of the noun. This is not a display name (anything
+ * being displayed needs to be localized, after all), but simply the
+ * canonical name for debugging purposes and for people to pass to
+ * lookupNoun. The suggested convention is lower-case-dash-delimited,
+ * with names being singular (since it's a single noun we are referring
+ * to.)
+ * @param class The 'class' to which an instance of the noun will belong (aka
+ * will pass an instanceof test).
+ * @param allowsArbitraryAttrs Is this a 'first class noun'/can it be a subject, AKA can
+ * this noun have attributes stored on it that relate it to other things?
+ * For example, a message is first-class; we store attributes of
+ * messages. A date is not first-class now, nor is it likely to be; we
+ * will not store attributes about a date, although dates will be the
+ * objects of other subjects. (For example: we might associate a date
+ * with a calendar event, but the date is an attribute of the calendar
+ * event and not vice versa.)
+ * @param usesParameter A boolean indicating whether this noun requires use
+ * of the 'parameter' BLOB storage field on the attribute bindings in the
+ * database to persist itself. Use of parameters should be limited
+ * to a reasonable number of values (16-32 is okay, more than that is
+ * pushing it and 256 should be considered an absolute upper bound)
+ * because of the database organization. When false, your toParamAndValue
+ * function is expected to return null for the parameter and likewise your
+ * fromParamAndValue should expect ignore and generally ignore the
+ * argument.
+ * @param toParamAndValue A function that takes an instantiated noun
+ * instance and returns a 2-element list of [parameter, value] where
+ * parameter may only be non-null if you passed a usesParameter of true.
+ * Parameter may be of any type (BLOB), and value must be numeric (pass
+ * 0 if you don't need the value).
+ */
+ defineNoun: function gloda_ns_defineNoun(aNounDef, aNounID) {
+ this._log.info("Defining noun: " + aNounDef.name);
+ if (aNounID === undefined)
+ aNounID = this._nextNounID++;
+ aNounDef.id = aNounID;
+ // if it has a table, you can query on it. seems straight-forward.
+ if (aNounDef.tableName) {
+ [aNounDef.queryClass, aNounDef.explicitQueryClass,
+ aNounDef.wildcardQueryClass] =
+ GlodaQueryClassFactory(aNounDef);
+ aNounDef._dbMeta = {};
+ aNounDef.class.prototype.NOUN_DEF = aNounDef;
+ aNounDef.toJSON = this._managedToJSON;
+
+ aNounDef.specialLoadAttribs = [];
+
+ // - define the 'id' constrainer
+ let idConstrainer = function() {
+ let constraint = [GlodaDatastore.kConstraintIdIn, null];
+ for (let iArg = 0; iArg < arguments.length; iArg++) {
+ constraint.push(arguments[iArg]);
+ }
+ this._constraints.push(constraint);
+ return this;
+ };
+ aNounDef.queryClass.prototype.id = idConstrainer;
+ }
+ if (aNounDef.cache) {
+ let cacheCost = aNounDef.cacheCost || 1024;
+ let cacheBudget = aNounDef.cacheBudget || 128 * 1024;
+ let cacheSize = Math.floor(cacheBudget / cacheCost);
+ if (cacheSize)
+ GlodaCollectionManager.defineCache(aNounDef, cacheSize);
+ }
+ aNounDef.attribsByBoundName = {};
+ aNounDef.domExposeAttribsByBoundName = {};
+
+ aNounDef.objectNounOfAttributes = [];
+
+ this._nounNameToNounID[aNounDef.name] = aNounID;
+ this._nounIDToDef[aNounID] = aNounDef;
+ aNounDef.actions = [];
+
+ this._attrProviderOrderByNoun[aNounDef.id] = [];
+ this._attrOptimizerOrderByNoun[aNounDef.id] = [];
+ this._attrProvidersByNoun[aNounDef.id] = {};
+ },
+
+ /**
+ * Lookup a noun (ID) suitable for passing to defineAttribute's various
+ * noun arguments. Throws an exception if the noun with the given name
+ * cannot be found; the assumption is that you can't live without the noun.
+ */
+ lookupNoun: function gloda_ns_lookupNoun(aNounName) {
+ if (aNounName in this._nounNameToNounID)
+ return this._nounNameToNounID[aNounName];
+
+ throw Error("Unable to locate noun with name '" + aNounName + "', but I " +
+ "do know about: " +
+ [propName for
+ (propName in this._nounNameToNounID)].join(", "));
+ },
+
+ /**
+ * Lookup a noun def given a name.
+ */
+ lookupNounDef: function gloda_ns_lookupNoun(aNounName) {
+ return this._nounIDToDef[this.lookupNoun(aNounName)];
+ },
+
+
+ /**
+ * Define an action on a noun. During the prototype stage, this was conceived
+ * of as a way to expose all the constraints possible given a noun. For
+ * example, if you have an identity or a contact, you could use this to
+ * see all the messages sent from/to a given contact. It was likewise
+ * thought potentially usable for future expansion. For example, you could
+ * also decide to send an e-mail to a contact when you have the contact
+ * instance available.
+ * Outside of the 'expmess' checkbox-happy prototype, this functionality is
+ * not used. As such, this functionality should be considered in flux and
+ * subject to changes. Also, very open to specific suggestsions motivated
+ * by use cases.
+ * One conceptual issue raised by this mechanism is the interaction of actions
+ * with facts like "this message is read". We currently implement the 'fact'
+ * by defining an attribute with a 'boolean' noun type. To deal with this,
+ * in various places we pass-in the attribute as well as the noun value.
+ * Since the relationships for booleans and integers in these cases is
+ * standard and well-defined, this works out pretty well, but suggests we
+ * need to think things through.
+ *
+ * @param aNounID The ID of the noun you want to define an action on.
+ * @param aActionMeta The dictionary describing the noun. The dictionary
+ * should have the following fields:
+ * - actionType: a string indicating the type of action. Currently, only
+ * "filter" is a legal value.
+ * - actionTarget: the noun ID of the noun type on which this action is
+ * applicable. For example,
+ *
+ * The following should be present for actionType=="filter";
+ * - shortName: The name that should be used to display this constraint. For
+ * example, a checkbox-heavy UI might display a checkbox for each constraint
+ * using shortName as the label.
+ * - makeConstraint: A function that takes the attribute that is the source
+ * of the noun and the noun instance as arguments, and returns APV-style
+ * constraints. Since the APV-style query mechanism is now deprecated,
+ * this signature is deprecated. Probably the way to update this would be
+ * to pass in the query instance that constraints should be contributed to.
+ */
+ defineNounAction: function gloda_ns_defineNounAction(aNounID, aActionMeta) {
+ let nounDef = this._nounIDToDef[aNounID];
+ nounDef.actions.push(aActionMeta);
+ },
+
+ /**
+ * Retrieve all of the actions (as defined using defineNounAction) for the
+ * given noun type (via noun ID) with the given action type (ex: filter).
+ */
+ getNounActions: function gloda_ns_getNounActions(aNounID, aActionType) {
+ let nounDef = this._nounIDToDef[aNounID];
+ if (!nounDef)
+ return [];
+ return [action for each ([i, action] in Iterator(nounDef.actions))
+ if (!aActionType || (action.actionType == aActionType))];
+ },
+
+ /** Attribute providers in the sequence to process them. */
+ _attrProviderOrderByNoun: {},
+ /** Attribute providers that provide optimizers, in the sequence to proc. */
+ _attrOptimizerOrderByNoun: {},
+ /** Maps attribute providers to the list of attributes they provide */
+ _attrProviders: {},
+ /**
+ * Maps nouns to their attribute providers to a list of the attributes they
+ * provide for the noun.
+ */
+ _attrProvidersByNoun: {},
+
+ /**
+ * Define the core nouns (that are not defined elsewhere) and a few noun
+ * actions. Core nouns could be defined in other files, assuming dependency
+ * issues are resolved via the everybody.js mechanism or something else.
+ * Right now, noun_tag defines the tag noun. If we broke more of these out,
+ * we would probably want to move the 'class' code from datamodel.js, the
+ * SQL table def and helper code from datastore.js (and this code) to their
+ * own noun_*.js files. There are some trade-offs to be made, and I think
+ * we can deal with those once we start to integrate lightning/calendar and
+ * our noun space gets large and more heterogeneous.
+ */
+ _initAttributes: function gloda_ns_initAttributes() {
+ this.defineNoun({
+ name: "bool",
+ class: Boolean, allowsArbitraryAttrs: false,
+ toParamAndValue: function(aBool) {
+ return [null, aBool ? 1 : 0];
+ }}, this.NOUN_BOOLEAN);
+ this.defineNoun({
+ name: "number",
+ class: Number, allowsArbitraryAttrs: false, continuous: true,
+ toParamAndValue: function(aNum) {
+ return [null, aNum];
+ }}, this.NOUN_NUMBER);
+ this.defineNoun({
+ name: "string",
+ class: String, allowsArbitraryAttrs: false,
+ toParamAndValue: function(aString) {
+ return [null, aString];
+ }}, this.NOUN_STRING);
+ this.defineNoun({
+ name: "date",
+ class: Date, allowsArbitraryAttrs: false, continuous: true,
+ toParamAndValue: function(aDate) {
+ return [null, aDate.valueOf() * 1000];
+ }}, this.NOUN_DATE);
+ this.defineNoun({
+ name: "fulltext",
+ class: String, allowsArbitraryAttrs: false, continuous: false,
+ // as noted on NOUN_FULLTEXT, we just pass the string around. it never
+ // hits the database, so it's okay.
+ toParamAndValue: function(aString) {
+ return [null, aString];
+ }}, this.NOUN_FULLTEXT);
+
+ this.defineNoun({
+ name: "folder",
+ class: GlodaFolder,
+ allowsArbitraryAttrs: false,
+ toParamAndValue: function(aFolderOrGlodaFolder) {
+ if (aFolderOrGlodaFolder instanceof GlodaFolder)
+ return [null, aFolderOrGlodaFolder.id];
+ else
+ return [null, GlodaDatastore._mapFolder(aFolderOrGlodaFolder).id];
+ }}, this.NOUN_FOLDER);
+ this.defineNoun({
+ name: "conversation",
+ class: GlodaConversation,
+ allowsArbitraryAttrs: false,
+ cache: true, cacheCost: 512,
+ tableName: "conversations",
+ attrTableName: "messageAttributes", attrIDColumnName: "conversationID",
+ datastore: GlodaDatastore,
+ objFromRow: GlodaDatastore._conversationFromRow,
+ toParamAndValue: function(aConversation) {
+ if (aConversation instanceof GlodaConversation)
+ return [null, aConversation.id];
+ else // assume they're just passing the id directly
+ return [null, aConversation];
+ }}, this.NOUN_CONVERSATION);
+ this.defineNoun({
+ name: "message",
+ class: GlodaMessage,
+ allowsArbitraryAttrs: true,
+ cache: true, cacheCost: 2048,
+ tableName: "messages",
+ attrTableName: "messageAttributes", attrIDColumnName: "messageID",
+ datastore: GlodaDatastore, objFromRow: GlodaDatastore._messageFromRow,
+ dbAttribAdjuster: GlodaDatastore.adjustMessageAttributes,
+ objInsert: GlodaDatastore.insertMessage,
+ objUpdate: GlodaDatastore.updateMessage,
+ toParamAndValue: function(aMessage) {
+ if (aMessage instanceof GlodaMessage)
+ return [null, aMessage.id];
+ else // assume they're just passing the id directly
+ return [null, aMessage];
+ }}, this.NOUN_MESSAGE);
+ this.defineNoun({
+ name: "contact",
+ class: GlodaContact,
+ allowsArbitraryAttrs: true,
+ cache: true, cacheCost: 128,
+ tableName: "contacts",
+ attrTableName: "contactAttributes", attrIDColumnName: "contactID",
+ datastore: GlodaDatastore, objFromRow: GlodaDatastore._contactFromRow,
+ dbAttribAdjuster: GlodaDatastore.adjustAttributes,
+ objInsert: GlodaDatastore.insertContact,
+ objUpdate: GlodaDatastore.updateContact,
+ toParamAndValue: function(aContact) {
+ if (aContact instanceof GlodaContact)
+ return [null, aContact.id];
+ else // assume they're just passing the id directly
+ return [null, aContact];
+ }}, this.NOUN_CONTACT);
+ this.defineNoun({
+ name: "identity",
+ class: GlodaIdentity,
+ allowsArbitraryAttrs: false,
+ cache: true, cacheCost: 128,
+ usesUniqueValue: true,
+ tableName: "identities",
+ datastore: GlodaDatastore, objFromRow: GlodaDatastore._identityFromRow,
+ toParamAndValue: function(aIdentity) {
+ if (aIdentity instanceof GlodaIdentity)
+ return [null, aIdentity.id];
+ else // assume they're just passing the id directly
+ return [null, aIdentity];
+ }}, this.NOUN_IDENTITY);
+
+ // parameterized identity is just two identities; we store the first one
+ // (whose value set must be very constrainted, like the 'me' identities)
+ // as the parameter, the second (which does not need to be constrained)
+ // as the value.
+ this.defineNoun({
+ name: "parameterized-identity",
+ class: null,
+ allowsArbitraryAttrs: false,
+ computeDelta: function(aCurValues, aOldValues) {
+ let oldMap = {};
+ for each (let [, tupe] in Iterator(aOldValues)) {
+ let [originIdentity, targetIdentity] = tupe;
+ let targets = oldMap[originIdentity];
+ if (targets === undefined)
+ targets = oldMap[originIdentity] = {};
+ targets[targetIdentity] = true;
+ }
+
+ let added = [], removed = [];
+ for each (let [, tupe] in Iterator(aCurValues)) {
+ let [originIdentity, targetIdentity] = tupe;
+ let targets = oldMap[originIdentity];
+ if ((targets === undefined) || !(targetIdentity in targets))
+ added.push(tupe);
+ else
+ delete targets[targetIdentity];
+ }
+
+ for each (let [originIdentity, targets] in Iterator(oldMap)) {
+ for (let targetIdentity in targets) {
+ removed.push([originIdentity, targetIdentity]);
+ }
+ }
+
+ return [added, removed];
+ },
+ contributeObjDependencies: function(aJsonValues, aReferencesByNounID,
+ aInverseReferencesByNounID) {
+ // nothing to do with a zero-length list
+ if (aJsonValues.length == 0)
+ return false;
+
+ let nounIdentityDef = Gloda._nounIDToDef[Gloda.NOUN_IDENTITY]
+ let references = aReferencesByNounID[nounIdentityDef.id];
+ if (references === undefined)
+ references = aReferencesByNounID[nounIdentityDef.id] = {};
+
+ for each (let [, tupe] in Iterator(aJsonValues)) {
+ let [originIdentityID, targetIdentityID] = tupe;
+ if (!(originIdentityID in references))
+ references[originIdentityID] = null;
+ if (!(targetIdentityID in references))
+ references[targetIdentityID] = null;
+ }
+
+ return true;
+ },
+ resolveObjDependencies: function(aJsonValues, aReferencesByNounID,
+ aInverseReferencesByNounID) {
+ let references =
+ aReferencesByNounID[Gloda.NOUN_IDENTITY];
+
+ let results = [];
+ for each (let [, tupe] in Iterator(aJsonValues)) {
+ let [originIdentityID, targetIdentityID] = tupe;
+ results.push([references[originIdentityID],
+ references[targetIdentityID]]);
+ }
+
+ return results;
+ },
+ toJSON: function (aIdentityTuple) {
+ return [aIdentityTuple[0].id, aIdentityTuple[1].id];
+ },
+ toParamAndValue: function(aIdentityTuple) {
+ return [aIdentityTuple[0].id, aIdentityTuple[1].id];
+ }}, this.NOUN_PARAM_IDENTITY);
+
+ GlodaDatastore.getAllAttributes();
+ },
+
+ /**
+ * Create accessor functions to 'bind' an attribute to underlying normalized
+ * attribute storage, as well as creating the appropriate query object
+ * constraint helper functions. This name is somewhat of a misnomer because
+ * special attributes are not 'bound' (because specific/non-generic per-class
+ * code provides the properties) but still depend on this method to
+ * establish their constraint helper methods.
+ *
+ * @XXX potentially rename to not suggest binding is required.
+ */
+ _bindAttribute: function gloda_ns_bindAttr(aAttrDef, aSubjectNounDef) {
+ let objectNounDef = aAttrDef.objectNounDef;
+
+ // -- the query constraint helpers
+ if (aSubjectNounDef.queryClass !== undefined) {
+ let constrainer;
+ // non-strings can use IN
+ if (aAttrDef.special == this.kSpecialFulltext) {
+ constrainer = function() {
+ let constraint = [GlodaDatastore.kConstraintFulltext, aAttrDef];
+ for (let iArg = 0; iArg < arguments.length; iArg++) {
+ constraint.push(arguments[iArg]);
+ }
+ this._constraints.push(constraint);
+ return this;
+ };
+ }
+ else if (aAttrDef.special != this.kSpecialString) {
+ constrainer = function() {
+ let constraint = [GlodaDatastore.kConstraintIn, aAttrDef];
+ for (let iArg = 0; iArg < arguments.length; iArg++) {
+ constraint.push(arguments[iArg]);
+ }
+ this._constraints.push(constraint);
+ return this;
+ };
+ }
+ else { // strings need to use equals for escaping reasons
+ // (we could introduce an 'escaped' in that we manually escape though)
+ constrainer = function() {
+ let constraint = [GlodaDatastore.kConstraintEquals, aAttrDef];
+ for (let iArg = 0; iArg < arguments.length; iArg++) {
+ constraint.push(arguments[iArg]);
+ }
+ this._constraints.push(constraint);
+ return this;
+ };
+ }
+
+ aSubjectNounDef.queryClass.prototype[aAttrDef.boundName] = constrainer;
+
+ // - ranged value helper: fooRange
+ if (objectNounDef.continuous) {
+ // takes one or more tuples of [lower bound, upper bound]
+ let rangedConstrainer = function() {
+ let constraint = [GlodaDatastore.kConstraintRanges, aAttrDef];
+ for (let iArg = 0; iArg < arguments.length; iArg++ ) {
+ constraint.push(arguments[iArg]);
+ }
+ this._constraints.push(constraints);
+ return this;
+ }
+
+ aSubjectNounDef.queryClass.prototype[aAttrDef.boundName + "Range"] =
+ rangedConstrainer;
+ }
+
+ // - string LIKE helper for special on-row attributes: fooLike
+ // (it is impossible to store a string as an indexed attribute, which is
+ // why we do this for on-row only.)
+ if (aAttrDef.special == this.kSpecialString) {
+ let likeConstrainer = function() {
+ let constraint = [GlodaDatastore.kConstraintStringLike, aAttrDef];
+ for (let iArg = 0; iArg < arguments.length; iArg++) {
+ constraint.push(arguments[iArg]);
+ }
+ this._constraints.push(constraint);
+ return this;
+ }
+
+ aSubjectNounDef.queryClass.prototype[aAttrDef.boundName + "Like"] =
+ likeConstrainer;
+ }
+ }
+ },
+
+ /**
+ * Define an attribute and all its meta-data. Takes a single dictionary as
+ * its argument, with the following required properties:
+ *
+ * @param provider The object instance providing a 'process' method.
+ * @param extensionName The name of the extension providing these attributes.
+ * @param attributeType The type of attribute, one of the values from the
+ * kAttr* enumeration.
+ * @param attributeName The name of the attribute, which also doubles as the
+ * bound property name if you pass 'bind' a value of true. You are
+ * responsible for avoiding collisions, which presumably will mean
+ * checking/updating a wiki page in the future, or just prefixing your
+ * attribute name with your extension name or something like that.
+ * @param bind Should this attribute be 'bound' as a convenience attribute
+ * on the subject's object (true/false)? For example, with an
+ * attributeName of "foo" and passing true for 'bind' with a subject noun
+ * of NOUN_MESSAGE, GlodaMessage instances will expose a "foo" getter
+ * that returns the value of the attribute. If 'singular' is true, this
+ * means an instance of the object class corresponding to the noun type or
+ * null if the attribute does not exist. If 'singular' is false, this
+ * means a list of instances of the object class corresponding to the noun
+ * type, where the list may be empty if no instances of the attribute are
+ * present.
+ * @param bindName Optional override of attributeName for purposes of the
+ * binding property's name.
+ * @param singular Is the attribute going to happen at most once (true),
+ * or potentially multiple times (false). This affects whether
+ * the binding returns a list or just a single item (which is null when
+ * the attribute is not present).
+ * @param subjectNouns A list of object types (NOUNs) that this attribute can
+ * be set on. Each element in the list should be one of the NOUN_*
+ * constants or a dynamically registered noun type.
+ * @param objectNoun The object type (one of the NOUN_* constants or a
+ * dynamically registered noun types) that is the 'object' in the
+ * traditional RDF triple. More pragmatically, in the database row used
+ * to represent an attribute, we store the subject (ex: message ID),
+ * attribute ID, and an integer which is the integer representation of the
+ * 'object' whose type you are defining right here.
+ */
+ defineAttribute: function gloda_ns_defineAttribute(aAttrDef) {
+ // ensure required properties exist on aAttrDef
+ if (!("provider" in aAttrDef) ||
+ !("extensionName" in aAttrDef) ||
+ !("attributeType" in aAttrDef) ||
+ !("attributeName" in aAttrDef) ||
+ !("singular" in aAttrDef) ||
+ !("subjectNouns" in aAttrDef) ||
+ !("objectNoun" in aAttrDef))
+ // perhaps we should have a list of required attributes, perchance with
+ // and explanation of what it holds, and use that to be friendlier?
+ throw Error("You omitted a required attribute defining property, please" +
+ " consult the documentation as penance.")
+
+ // return if the attribute has already been defined
+ if (aAttrDef.dbDef) {
+ return aAttrDef;
+ }
+
+ // provider tracking
+ if (!(aAttrDef.provider.providerName in this._attrProviders)) {
+ this._attrProviders[aAttrDef.provider.providerName] = [];
+ }
+
+ let compoundName = aAttrDef.extensionName + ":" + aAttrDef.attributeName;
+ let attrDBDef;
+ if (compoundName in GlodaDatastore._attributeDBDefs) {
+ // the existence of the GlodaAttributeDBDef means that either it has
+ // already been fully defined, or has been loaded from the database but
+ // not yet 'bound' to a provider (and had important meta-info that
+ // doesn't go in the db copied over)
+ attrDBDef = GlodaDatastore._attributeDBDefs[compoundName];
+ }
+ // we need to create the attribute definition in the database
+ else {
+ let attrID = null;
+ attrID = GlodaDatastore._createAttributeDef(aAttrDef.attributeType,
+ aAttrDef.extensionName,
+ aAttrDef.attributeName,
+ null);
+
+ attrDBDef = new GlodaAttributeDBDef(GlodaDatastore, attrID, compoundName,
+ aAttrDef.attributeType, aAttrDef.extensionName, aAttrDef.attributeName);
+ GlodaDatastore._attributeDBDefs[compoundName] = attrDBDef;
+ GlodaDatastore._attributeIDToDBDefAndParam[attrID] = [attrDBDef, null];
+ }
+
+ aAttrDef.dbDef = attrDBDef;
+ attrDBDef.attrDef = aAttrDef;
+
+ aAttrDef.id = aAttrDef.dbDef.id;
+
+ if ("bindName" in aAttrDef)
+ aAttrDef.boundName = aAttrDef.bindName;
+ else
+ aAttrDef.boundName = aAttrDef.attributeName;
+
+ aAttrDef.objectNounDef = this._nounIDToDef[aAttrDef.objectNoun];
+ aAttrDef.objectNounDef.objectNounOfAttributes.push(aAttrDef);
+
+ for (let iSubject = 0; iSubject < aAttrDef.subjectNouns.length;
+ iSubject++) {
+ let subjectType = aAttrDef.subjectNouns[iSubject];
+ let subjectNounDef = this._nounIDToDef[subjectType];
+ this._bindAttribute(aAttrDef, subjectNounDef);
+
+ // update the provider maps...
+ if (this._attrProviderOrderByNoun[subjectType]
+ .indexOf(aAttrDef.provider) == -1) {
+ this._attrProviderOrderByNoun[subjectType].push(aAttrDef.provider);
+ if (aAttrDef.provider.optimize)
+ this._attrOptimizerOrderByNoun[subjectType].push(aAttrDef.provider);
+ this._attrProvidersByNoun[subjectType][aAttrDef.provider] = [];
+ }
+ this._attrProvidersByNoun[subjectType][aAttrDef.provider].push(aAttrDef);
+
+ subjectNounDef.attribsByBoundName[aAttrDef.boundName] = aAttrDef;
+ if (aAttrDef.domExpose)
+ subjectNounDef.domExposeAttribsByBoundName[aAttrDef.boundName] =
+ aAttrDef;
+
+ if (aAttrDef.special & this.kSpecialColumn)
+ subjectNounDef.specialLoadAttribs.push(aAttrDef);
+
+ // if this is a parent column attribute, make note of it so that if we
+ // need to do an inverse references lookup, we know what column we are
+ // issuing against.
+ if (aAttrDef.special === this.kSpecialColumnParent) {
+ subjectNounDef.parentColumnAttr = aAttrDef;
+ }
+
+ if (aAttrDef.objectNounDef.tableName ||
+ aAttrDef.objectNounDef.contributeObjDependencies) {
+ subjectNounDef.hasObjDependencies = true;
+ }
+ }
+
+ this._attrProviders[aAttrDef.provider.providerName].push(aAttrDef);
+ return aAttrDef;
+ },
+
+ /**
+ * Retrieve the attribute provided by the given extension with the given
+ * attribute name. The original idea was that plugins would effectively
+ * name-space attributes, helping avoid collisions. Since we are leaning
+ * towards using binding heavily, this doesn't really help, as the collisions
+ * will just occur on the attribute name instead. Also, this can turn
+ * extensions into liars as name changes/moves to core/etc. happen.
+ * @TODO consider removing the extension name argument parameter requirement
+ */
+ getAttrDef: function gloda_ns_getAttrDef(aPluginName, aAttrName) {
+ let compoundName = aPluginName + ":" + aAttrName;
+ return GlodaDatastore._attributeDBDefs[compoundName];
+ },
+
+ /**
+ * Define a SQL table for plug-ins. This is intended to be used by
+ * extensions/plug-ins whose storage needs exceed those provided by the
+ * attribute parameter (on the attribute definition)/attribute value (on the
+ * attribute instance) idiom. (This includes extensions whose parameter
+ * usage would exceed acceptable cardinality.) They can create a table
+ * to store information on their nouns, using their row id (commonly "id")
+ * as the attribute value.
+ * The current implementation was for a prototype and this should not be
+ * interpreted as our final approach. Our goal is just to make it easy to
+ * add your own data-type and have it interact with the rest of the gloda
+ * schema. We don't really want to be a be-all, end-all JS ORM (object
+ * relational mapper), though we started down that road.
+ *
+ * The argument should be a dictionary with the following keys:
+ * @param name The table name; don't conflict with other things!
+ * @param columns A list of [column name, sqlite type] tuples. You should
+ * always include a definition like ["id", "INTEGER PRIMARY KEY"] for
+ * now.
+ * @param indices A dictionary of lists of column names, where the key name
+ * becomes the index name. Ex: {foo: ["bar"]} results in an index on
+ * the column "bar" where the index is named "foo".
+ */
+ defineTable: function gloda_ns_defineTable(aTableDef) {
+ return GlodaDatastore.createTableIfNotExists(aTableDef);
+ },
+
+ /**
+ * Create a new query instance for the given noun-type. This provides
+ * a generic way to provide constraint-based queries of any first-class
+ * nouns supported by the system.
+ *
+ * The idea is that every attribute on an object can be used to express
+ * a constraint on the query object. Constraints implicitly 'AND' together,
+ * but providing multiple arguments to a constraint function results in an
+ * 'OR'ing of those values. Additionally, you can call or() on the returned
+ * query to create an alternate query that is effectively a giant OR against
+ * all the constraints you create on the main query object (or any other
+ * alternate queries returned by or()). (Note: there is no nesting of these
+ * alternate queries. query.or().or() is equivalent to query.or())
+ * For each attribute, there is a constraint with the same name that takes
+ * one or more arguments. The arguments represent a set of OR values that
+ * objects matching the query can have. (If you want the constraint
+ * effectively ANDed together, just invoke the constraint function
+ * multiple times.) For example, newQuery(NOUN_PERSON).age(25) would
+ * constraint to all the people aged 25, while age(25, 26) would constrain
+ * to all the people age 25 or 26.
+ * For each attribute with a 'continuous' noun, there is a constraint with the
+ * attribute name with "Range" appended. It takes two arguments which are an
+ * inclusive lower bound and an inclusive lower bound for values in the
+ * range. If you would like an open-ended range on either side, pass null
+ * for that argument. If you would like to specify multiple ranges that
+ * should be ORed together, simply pass additional (pairs of) arguments.
+ * For example, newQuery(NOUN_PERSON).age(25,100) would constraint to all
+ * the people who are >= 25 and <= 100. Likewise age(25, null) would just
+ * return all the people who are 25 or older. And age(25,30,35,40) would
+ * return people who are either 25-30 or 35-30.
+ * There are also full-text constraint columns. In a nutshell, their
+ * arguments are the strings that should be passed to the SQLite FTS3
+ * MATCH clause.
+ */
+ newQuery: function gloda_ns_newQuery(aNounID) {
+ let nounDef = this._nounIDToDef[aNounID];
+ return new nounDef.queryClass();
+ },
+
+ /**
+ * Create a collection/query for the given noun-type that only matches the
+ * provided items. This is to be used when you have an explicit set of items
+ * that you would still like to receive updates for.
+ */
+ explicitCollection: function gloda_ns_explicitCollection(aNounID, aItems) {
+ let nounDef = this._nounIDToDef[aNounID];
+ let collection = new GlodaCollection(nounDef, aItems, null, null)
+ let query = new nounDef.explicitQueryClass(collection);
+ collection.query = query;
+ GlodaCollectionManager.registerCollection(collection);
+ return collection;
+ },
+
+ /**
+ * Debugging 'wildcard' collection creation support. A wildcard collection
+ * will 'accept' any new item instances presented to the collection manager
+ * as new. The result is that it allows you to be notified as new items
+ * as they are indexed, existing items as they are loaded from the database,
+ * etc.
+ * Because the items are added to the collection without limit, this will
+ * result in a leak if you don't do something to clean up after the
+ * collection. (Forgetting about the collection will suffice, as it is still
+ * weakly held.)
+ */
+ _wildcardCollection: function gloda_ns_wildcardCollection(aNounID, aItems) {
+ let nounDef = this._nounIDToDef[aNounID];
+ let collection = new GlodaCollection(nounDef, aItems, null, null)
+ let query = new nounDef.wildcardQueryClass(collection);
+ collection.query = query;
+ GlodaCollectionManager.registerCollection(collection);
+ return collection;
+ },
+
+ /**
+ * Populate a gloda representation of an item given the thus-far built
+ * representation, the previous representation, and one or more raw
+ * representations.
+ *
+ * The result of the processing ends up with attributes in 3 different forms:
+ * - Database attribute rows (to be added and removed).
+ * - In-memory representation.
+ * - JSON-able representation.
+ */
+ grokNounItem: function gloda_ns_grokNounItem(aItem, aRawReps, aIsNew,
+ aCallbackHandle, aDoCache) {
+ let itemNounDef = this._nounIDToDef[aItem.NOUN_ID];
+ let attribsByBoundName = itemNounDef.attribsByBoundName;
+
+ this._log.info(" ** grokNounItem: " + itemNounDef.name);
+
+ let addDBAttribs = [];
+ let removeDBAttribs = [];
+
+ let jsonDict = {};
+
+ let aOldItem;
+ if (aIsNew) // there is no old item if we are new.
+ aOldItem = {};
+ else {
+ aOldItem = aItem;
+ // we want to create a clone of the existing item so that we can know the
+ // deltas that happened for indexing purposes
+ aItem = aItem._clone();
+ }
+
+ // Have the attribute providers directly set properties on the aItem
+ let attrProviders = this._attrProviderOrderByNoun[aItem.NOUN_ID];
+ for (let iProvider = 0; iProvider < attrProviders.length; iProvider++) {
+ this._log.info(" * provider: " + attrProviders[iProvider].providerName);
+ yield aCallbackHandle.pushAndGo(
+ attrProviders[iProvider].process(aItem, aRawReps, aIsNew,
+ aCallbackHandle));
+ }
+
+ let attrOptimizers = this._attrOptimizerOrderByNoun[aItem.NOUN_ID];
+ for (let iProvider = 0; iProvider < attrOptimizers.length; iProvider++) {
+ this._log.info(" * optimizer: " + attrOptimizers[iProvider].providerName);
+ yield aCallbackHandle.pushAndGo(
+ attrOptimizers[iProvider].optimize(aItem, aRawReps, aIsNew,
+ aCallbackHandle));
+ }
+
+ this._log.info(" ** done with providers.");
+
+ // Iterate over the attributes on the item
+ for each (let [key, value] in Iterator(aItem)) {
+ // ignore keys that start with underscores, they are private and not
+ // persisted by our attribute mechanism. (they are directly handled by
+ // the object implementation.)
+ if (key[0] == "_")
+ continue;
+ // find the attribute definition that corresponds to this key
+ let attrib = attribsByBoundName[key];
+ // if there's no attribute, that's not good, but not horrible.
+ if (attrib === undefined) {
+ this._log.warn("new proc ignoring attrib: " + key);
+ continue;
+ }
+
+ let attribDB = attrib.dbDef;
+ let objectNounDef = attrib.objectNounDef;
+
+ // - translate for our JSON rep
+ if (attrib.singular) {
+ if (objectNounDef.toJSON)
+ jsonDict[attrib.id] = objectNounDef.toJSON(value);
+ else
+ jsonDict[attrib.id] = value;
+ }
+ else {
+ if (objectNounDef.toJSON) {
+ toJSON = objectNounDef.toJSON;
+ jsonDict[attrib.id] = [toJSON(subValue) for each
+ ([, subValue] in Iterator(value))] ;
+ }
+ else
+ jsonDict[attrib.id] = value;
+ }
+
+ // perform a delta analysis against the old value, if we have one
+ let oldValue = aOldItem[key];
+ if (oldValue !== undefined) {
+ // in the singular case if they don't match, it's one add and one remove
+ if (attrib.singular) {
+ // test for identicality, failing that, see if they have explicit
+ // equals support.
+ if ((value !== oldValue) &&
+ (!value.equals || !value.equals(oldValue))) {
+ this._log.debug("%% want to add1 " + value + " which map to " + attribDB.convertValuesToDBAttributes([value]));
+ this._log.debug("%% want to rem1 " + oldValue + " which map to " + attribDB.convertValuesToDBAttributes([oldValue]));
+ addDBAttribs.push(attribDB.convertValuesToDBAttributes([value])[0]);
+ removeDBAttribs.push(
+ attribDB.convertValuesToDBAttributes([oldValue])[0]);
+ }
+ }
+ // in the plural case, we have to figure the deltas accounting for
+ // possible changes in ordering (which is insignificant from an
+ // indexing perspective)
+ // some nouns may not meet === equivalence needs, so must provide a
+ // custom computeDelta method to help us out
+ else if (objectNounDef.computeDelta) {
+ let [valuesAdded, valuesRemoved] =
+ objectNounDef.computeDelta(value, oldValue);
+ // convert the values to database-style attribute rows
+ this._log.debug("%% cdelta want to add " + valuesAdded + " which map to " + attribDB.convertValuesToDBAttributes(valuesAdded));
+ this._log.debug("%% cdelta want to rem " + valuesRemoved + " which map to " + attribDB.convertValuesToDBAttributes(valuesRemoved));
+ addDBAttribs.push.apply(addDBAttribs,
+ attribDB.convertValuesToDBAttributes(valuesAdded));
+ removeDBAttribs.push.apply(removeDBAttribs,
+ attribDB.convertValuesToDBAttributes(valuesRemoved));
+ }
+ else {
+ // build a map of the previous values; we will delete the values as
+ // we see them so that we will know what old values are no longer
+ // present in the current set of values.
+ let oldValueMap = {};
+ for each (let [, anOldValue] in Iterator(oldValue)) {
+this._log.debug(" old traverse: " + anOldValue);
+ // remember, the key is just the toString'ed value, so we need to
+ // store and use the actual value as the value!
+ oldValueMap[anOldValue] = anOldValue;
+ }
+ // traverse the current values...
+ let valuesAdded = [];
+ for each (let [, curValue] in Iterator(value)) {
+this._log.debug(" new traverse: " + curValue);
+ if (curValue in oldValueMap)
+ delete oldValueMap[curValue];
+ else
+ valuesAdded.push(curValue);
+ }
+ // anything still on oldValueMap was removed.
+ let valuesRemoved = [val for each (val in oldValueMap)];
+ // convert the values to database-style attribute rows
+ this._log.debug("%% want to add " + valuesAdded + " which map to " + attribDB.convertValuesToDBAttributes(valuesAdded));
+ this._log.debug("%% want to rem " + valuesRemoved + " which map to " + attribDB.convertValuesToDBAttributes(valuesRemoved));
+ addDBAttribs.push.apply(addDBAttribs,
+ attribDB.convertValuesToDBAttributes(valuesAdded));
+ removeDBAttribs.push.apply(removeDBAttribs,
+ attribDB.convertValuesToDBAttributes(valuesRemoved));
+ }
+
+ // replace the old value with the new values... (the 'old' item is
+ // canonical)
+ aOldItem[key] = value;
+ }
+ // no old value, all values are new
+ else {
+ // the 'old' item is still the canonical one; update it
+ if (!aIsNew)
+ aOldItem[key] = value;
+ // add the db reps on the new values
+ if (attrib.singular)
+ value = [value];
+ this._log.debug("%% no old, want to add " + value + " which map to " + attribDB.convertValuesToDBAttributes(value));
+ addDBAttribs.push.apply(addDBAttribs,
+ attribDB.convertValuesToDBAttributes(value));
+ }
+ }
+
+ // Iterate over any remaining values in old items for purge purposes.
+ for each (let [key, value] in Iterator(aOldItem)) {
+ // ignore keys that start with underscores, they are private and not
+ // persisted by our attribute mechanism. (they are directly handled by
+ // the object implementation.)
+ if (key[0] == "_")
+ continue;
+ // ignore things we saw in the new guy
+ if (key in aItem)
+ continue;
+
+ // find the attribute definition that corresponds to this key
+ let attrib = attribsByBoundName[key];
+ // if there's no attribute, that's not good, but not horrible.
+ if (attrib === undefined) {
+ this._log.warn("old proc ignoring attrib: " + key);
+ continue;
+ }
+ let attribDB = attrib.dbDef;
+ this._log.debug("%% want to remove " + value + " which map to " + attribDB.convertValuesToDBAttributes(value));
+ removeDBAttribs.push.apply(removeDBAttribs,
+ attribDB.convertValuesToDBAttributes(value));
+ // delete these from the old item, as the old item is canonical, and
+ // should no longer have these values
+ delete aOldItem[key];
+ }
+
+ aItem._jsonText = this._json.encode(jsonDict);
+ this._log.debug(" json text: " + aItem._jsonText);
+
+ if (aIsNew) {
+ this._log.debug(" inserting item");
+ itemNounDef.objInsert.call(itemNounDef.datastore, aItem);
+ }
+ else {
+ this._log.debug(" updating item");
+ itemNounDef.objUpdate.call(itemNounDef.datastore, aItem);
+ }
+
+ this._log.debug(" adjusting attributes, add: " + addDBAttribs + " rem: " +
+ removeDBAttribs);
+ itemNounDef.dbAttribAdjuster.call(itemNounDef.datastore, aItem,
+ addDBAttribs, removeDBAttribs);
+
+ // Cache ramifications...
+ if (aDoCache === undefined || aDoCache) {
+ if (aIsNew)
+ GlodaCollectionManager.itemsAdded(aItem.NOUN_ID, [aItem]);
+ else
+ GlodaCollectionManager.itemsModified(aOldItem.NOUN_ID, [aOldItem]);
+ }
+
+ this._log.debug(" done grokking.");
+
+ yield this.kWorkDone;
+ },
+};
+
+/* and initialize the Gloda object/NS before we return... */
+try {
+ Gloda._init();