mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-24 09:27:31 +09:00
2283 lines
93 KiB
JavaScript
2283 lines
93 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
this.EXPORTED_SYMBOLS = ['Gloda'];
|
|
|
|
var Cc = Components.classes;
|
|
var Ci = Components.interfaces;
|
|
var Cr = Components.results;
|
|
var Cu = Components.utils;
|
|
|
|
Cu.import("resource:///modules/gloda/log4moz.js");
|
|
|
|
Cu.import("resource:///modules/gloda/datastore.js");
|
|
Cu.import("resource:///modules/gloda/datamodel.js");
|
|
Cu.import("resource:///modules/gloda/databind.js");
|
|
Cu.import("resource:///modules/gloda/collection.js");
|
|
Cu.import("resource:///modules/gloda/connotent.js");
|
|
Cu.import("resource:///modules/gloda/query.js");
|
|
Cu.import("resource:///modules/gloda/utils.js");
|
|
|
|
Cu.import("resource:///modules/iteratorUtils.jsm");
|
|
Cu.import("resource:///modules/IOUtils.js");
|
|
Cu.import("resource:///modules/mailServices.js");
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
|
|
/**
|
|
* @see |Gloda.BadItemContentsError|
|
|
*/
|
|
function BadItemContentsError(aMessage) {
|
|
this.message = aMessage;
|
|
}
|
|
BadItemContentsError.prototype = {
|
|
toString: function BadItemContentsError_toString() {
|
|
return this.message;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* 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();
|
|
GlodaDatastore._init(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();
|
|
Log4Moz.repository.rootLogger.level = Log4Moz.Level.Debug;
|
|
|
|
let enableConsoleLogging = false;
|
|
let enableDumpLogging = false;
|
|
// should we assume there is someone else consuming our log4moz stream?
|
|
let enableUpstreamLogging = false;
|
|
let considerNetLogging = false;
|
|
|
|
let glodaLog = Log4Moz.repository.getLogger("gloda");
|
|
glodaLog.level = Log4Moz.Level.Warn;
|
|
|
|
try {
|
|
// figure out if event-driven indexing should be enabled...
|
|
let branch = Services.prefs.getBranch("mailnews.database.global.logging.");
|
|
enableConsoleLogging = branch.getBoolPref("console");
|
|
enableDumpLogging = branch.getBoolPref("dump");
|
|
enableUpstreamLogging = branch.getBoolPref("upstream");
|
|
considerNetLogging = branch.getBoolPref("net");
|
|
} catch (ex) {}
|
|
|
|
if (enableConsoleLogging) {
|
|
let capp = new Log4Moz.ConsoleAppender(formatter);
|
|
capp.level = Log4Moz.Level.Warn;
|
|
glodaLog.addAppender(capp);
|
|
}
|
|
|
|
if (enableDumpLogging) {
|
|
let dapp = new Log4Moz.DumpAppender(formatter);
|
|
dapp.level = Log4Moz.Level.All;
|
|
glodaLog.level = Log4Moz.Level.All;
|
|
glodaLog.addAppender(dapp);
|
|
}
|
|
|
|
if (enableUpstreamLogging) {
|
|
glodaLog.level = Log4Moz.Level.All;
|
|
}
|
|
|
|
if (considerNetLogging) {
|
|
let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
|
|
file.append("chainsaw.ptr");
|
|
if (file.exists()) {
|
|
let data = IOUtils.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;
|
|
glodaLog.level = Log4Moz.Level.All;
|
|
glodaLog.addAppender(sapp);
|
|
}
|
|
}
|
|
|
|
this._log = Log4Moz.repository.getLogger("gloda.NS");
|
|
this._log.info("Logging Initialized");
|
|
},
|
|
|
|
/**
|
|
* The indexer is idle.
|
|
*/
|
|
kIndexerIdle: 0,
|
|
/**
|
|
* The indexer is doing something. We used to have other specific states, but
|
|
* they have been rendered irrelevant and wiped from existence.
|
|
*/
|
|
kIndexerIndexing: 1,
|
|
|
|
/**
|
|
* Synchronous activities performed that can be thought of as one processing
|
|
* token. Potentially yield the event-loop and re-schedule for later based
|
|
* on how long we've actually taken/etc. The goal here is that code that
|
|
* is doing stuff synchronously yields with kWorkSync periodically to make
|
|
* sure that it doesn't dominate the event-loop. Unless the processing
|
|
* in question is particularly intensive, it should be reasonable to apply
|
|
* some decimation factor (ex: 32 or 64) with the general goal of yielding
|
|
* every 3-10 milliseconds.
|
|
*/
|
|
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 because we believe we have tied up the event loop for a
|
|
* non-trivial amount of time. So please re-schedule us in the future.
|
|
*
|
|
* This is currently only used internally by the indexer's batching logic;
|
|
* minor changes may be required if used by actual indexers.
|
|
*/
|
|
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,
|
|
|
|
/**
|
|
* Callers should access the unique ID for the GlodaDatastore
|
|
* with this getter. If the GlodaDatastore has not been
|
|
* initialized, this value is null.
|
|
*
|
|
* @return a UUID as a string, ex: "c4dd0159-9287-480f-a648-a4613e147fdb"
|
|
*/
|
|
get datastoreID() {
|
|
return GlodaDatastore._datastoreID;
|
|
},
|
|
|
|
/**
|
|
* Lookup a gloda message from an nsIMsgDBHdr, with the result returned as a
|
|
* collection. Keep in mind that the message may not be indexed, so you
|
|
* may end up with an empty collection. (Also keep in mind that this query
|
|
* is asynchronous, so you will want your action-taking logic to be found
|
|
* in your listener's onQueryCompleted method; the result will not be in
|
|
* the collection when this method returns.)
|
|
*
|
|
* @param aMsgHdr The header of the message you want the gloda message for.
|
|
* @param aListener The listener that should be registered with the collection
|
|
* @param aData The (optional) value to set as the data attribute on the
|
|
* collection.
|
|
*
|
|
* @return The collection that will receive the results.
|
|
*
|
|
* @testpoint gloda.ns.getMessageCollectionForHeader()
|
|
*/
|
|
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);
|
|
},
|
|
|
|
/**
|
|
* Given a list of message headers, return a collection containing the gloda
|
|
* messages that correspond to those headers. Keep in mind that gloda may
|
|
* not have indexed all the messages, so the returned collection may not have
|
|
* a message for each header you provide. (Also keep in mind that this query
|
|
* is asynchronous, so you will want your action-taking logic to be found
|
|
* in your listener's onQueryCompleted method; no results will be present in
|
|
* the collection when this method returns.)
|
|
*
|
|
* @param aHeaders A javascript Array or and XPCOM list that fixIterator can
|
|
* can handle.
|
|
* @param aListener The listener that should be registered with the collection
|
|
* @param aData The (optional) value to set as the data attribute on the
|
|
* collection.
|
|
*
|
|
* @return The collection that will receive the results.
|
|
*
|
|
* @testpoint gloda.ns.getMessageCollectionForHeaders()
|
|
*/
|
|
getMessageCollectionForHeaders: function gloda_ns_getMessagesForHeaders(
|
|
aHeaders, aListener, aData) {
|
|
// group the headers by the folder they are found in
|
|
let headersByFolder = {};
|
|
let iter;
|
|
for (let header in fixIterator(aHeaders)) {
|
|
let folderURI = header.folder.URI;
|
|
let headersForFolder = headersByFolder[folderURI];
|
|
if (headersForFolder === undefined)
|
|
headersByFolder[folderURI] = [header];
|
|
else
|
|
headersForFolder.push(header);
|
|
}
|
|
|
|
let query = Gloda.newQuery(Gloda.NOUN_MESSAGE);
|
|
let clause;
|
|
// build a query, using a separate union clause for each folder.
|
|
for (let folderURI in headersByFolder) {
|
|
let headersForFolder = headersByFolder[folderURI];
|
|
let folder = this.getFolderForFolder(headersForFolder[0].folder);
|
|
// if this is the first or clause, just use the query itself
|
|
if (!clause)
|
|
clause = query;
|
|
else // create a new query clause via the 'or' command
|
|
clause = query.or();
|
|
|
|
clause.folder(folder);
|
|
let messageKeys = headersForFolder.map(hdr => hdr.messageKey);
|
|
clause.messageKey.apply(clause, messageKeys);
|
|
}
|
|
|
|
return query.getCollection(aListener, aData);
|
|
},
|
|
|
|
/**
|
|
* @testpoint gloda.ns.getMessageContent
|
|
*/
|
|
getMessageContent: function gloda_ns_getMessageContent(aGlodaMessage, aMimeMsg) {
|
|
return mimeMsgToContentAndMeta(aMimeMsg, aGlodaMessage.folderMessage.folder)[0];
|
|
},
|
|
|
|
getFolderForFolder: function gloda_ns_getFolderForFolder(aMsgFolder) {
|
|
return GlodaDatastore._mapFolder(aMsgFolder);
|
|
},
|
|
|
|
/**
|
|
* Takes one or more strings containing lists of comma-delimited e-mail
|
|
* addresses with optional display names, and returns a list of sub-lists of
|
|
* identities, where each sub-list corresponds to each of the strings passed
|
|
* as arguments. These identities are loaded from the database if they
|
|
* already exist, or created if they do not yet exist.
|
|
* If the identities need to be created, they will also result in the
|
|
* creation of a gloda contact. If a display name was provided with the
|
|
* e-mail address, it will become the name of the gloda contact. If a
|
|
* display name was not provided, the e-mail address will also serve as the
|
|
* contact name.
|
|
* This method uses the indexer's callback handle mechanism, and does not
|
|
* obey traditional return semantics.
|
|
*
|
|
* We normalize all e-mail addresses to be lowercase as a normative measure.
|
|
*
|
|
* @param aCallbackHandle The GlodaIndexer callback handle (or equivalent)
|
|
* that you are operating under.
|
|
* @param ... One or more strings. Each string can contain zero or more
|
|
* e-mail addresses with display name. If more than one address is given,
|
|
* they should be comma-delimited. For example
|
|
* '"Bob Smith" <bob@example.com>' is an address with display name. Mime
|
|
* header decoding is performed, but is ignorant of any folder-level
|
|
* character set overrides.
|
|
* @returns via the callback handle mechanism, a list containing one sub-list
|
|
* for each string argument passed. Each sub-list containts zero or more
|
|
* GlodaIdentity instances corresponding to the addresses provided.
|
|
*/
|
|
getOrCreateMailIdentities:
|
|
function* gloda_ns_getOrCreateMailIdentities(aCallbackHandle) {
|
|
let addresses = {};
|
|
let resultLists = [];
|
|
|
|
// parse the strings
|
|
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].toLowerCase();
|
|
if (address in addresses)
|
|
addresses[address].push(resultList);
|
|
else
|
|
addresses[address] = [parsed.names[iAddress], resultList];
|
|
}
|
|
}
|
|
|
|
let addressList = Object.keys(addresses);
|
|
if (addressList.length == 0) {
|
|
yield aCallbackHandle.doneWithResult(resultLists);
|
|
// we should be stopped before we reach this point, but safety first.
|
|
return;
|
|
}
|
|
|
|
let query = this.newQuery(this.NOUN_IDENTITY);
|
|
query.kind("email");
|
|
query.value.apply(query, addressList);
|
|
let collection = query.getCollection(aCallbackHandle);
|
|
yield this.kWorkAsync;
|
|
|
|
// put the identities in the appropriate result lists
|
|
for (let identity of 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 (let address in addresses) {
|
|
let nameAndResultLists = addresses[address];
|
|
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: card}, true, 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, fallbackName;
|
|
let existingIdentities = [];
|
|
let identitiesToCreate = [];
|
|
|
|
let numIdentities = MailServices.accounts.allIdentities.length;
|
|
|
|
// nothing to do if there are no accounts/identities.
|
|
if (!numIdentities)
|
|
return;
|
|
|
|
for (let iIdentity = 0; iIdentity < numIdentities; iIdentity++) {
|
|
let msgIdentity =
|
|
MailServices.accounts.allIdentities.queryElementAt(iIdentity,
|
|
Ci.nsIMsgIdentity);
|
|
|
|
if (!fullName)
|
|
fullName = msgIdentity.fullName;
|
|
if (!fallbackName)
|
|
fallbackName = msgIdentity.email;
|
|
|
|
let emailAddress = msgIdentity.email;
|
|
let replyTo = msgIdentity.replyTo;
|
|
|
|
// find the identities if they exist, flag to create them if they don't
|
|
if (emailAddress) {
|
|
let 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) {
|
|
let 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 (let identity of 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 || fallbackName,
|
|
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 = myIdentities;
|
|
myContact._identities = Object.keys(myIdentities).
|
|
map(id => myIdentities[id]);
|
|
|
|
// we need contacts to make these objects reachable via the collection
|
|
// manager.
|
|
this._myContactCollection = this.explicitCollection(this.NOUN_CONTACT,
|
|
[this.myContact]);
|
|
this._myIdentitiesCollection =
|
|
this.explicitCollection(this.NOUN_IDENTITY, this.myContact._identities);
|
|
},
|
|
|
|
/**
|
|
* 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, which once had
|
|
* query ramifications and one day may have them again.
|
|
*/
|
|
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",
|
|
|
|
/**
|
|
* Special sentinel value that will cause facets to skip a noun instance
|
|
* when an attribute has this value.
|
|
*/
|
|
IGNORE_FACET: GlodaDatastore.IGNORE_FACET,
|
|
|
|
/*
|
|
* 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
|
|
/**
|
|
* An attachment to a message. A message may have many different attachments.
|
|
*/
|
|
NOUN_ATTACHMENT: GlodaAttachment.prototype.NOUN_ID, // 105
|
|
/**
|
|
* An account related to a message. A message can have only one account.
|
|
*/
|
|
NOUN_ACCOUNT: GlodaAccount.prototype.NOUN_ID, // 106
|
|
|
|
/**
|
|
* 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 aNounDef.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 aNounDef.class The 'class' to which an instance of the noun will
|
|
* belong (aka will pass an instanceof test). You may also provide this
|
|
* as 'clazz' if the keyword makes your IDE angry.
|
|
* @param aNounDef.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 aNounDef.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 aNounDef.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).
|
|
*
|
|
* @param aNounDef.isPrimitive True when the noun instance is a raw numeric
|
|
* value/string/boolean. False when the instance is an object. When
|
|
* false, it is assumed the attribute that serves as a unique identifier
|
|
* for the value is "id" unless 'idAttr' is provided.
|
|
* @param [aNounDef.idAttr="id"] For non-primitive nouns, this is the
|
|
* attribute on the object that uniquely identifies it.
|
|
*
|
|
* @param aNounDef.schema Unsupported mechanism by which you can define a
|
|
* table that corresponds to this noun. The table will be created if it
|
|
* does not exist.
|
|
* - name The table name; don't conflict with other things!
|
|
* - columns A list of [column name, sqlite type] tuples. You should
|
|
* always include a definition like ["id", "INTEGER PRIMARY KEY"] for
|
|
* now (and it should be the first column name too.) If you care about
|
|
* how the attributes are poked into your object (for example, you want
|
|
* underscores used for some of them because the attributes should be
|
|
* immutable), then you can include a third string that is the name of
|
|
* the attribute to use.
|
|
* - 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".
|
|
*/
|
|
defineNoun: function gloda_ns_defineNoun(aNounDef, aNounID) {
|
|
this._log.info("Defining noun: " + aNounDef.name);
|
|
if (aNounID === undefined)
|
|
aNounID = this._nextNounID++;
|
|
aNounDef.id = aNounID;
|
|
|
|
// Let people whose editors get angry about illegal attribute names use
|
|
// clazz instead of class.
|
|
if (aNounDef.clazz)
|
|
aNounDef.class = aNounDef.clazz;
|
|
|
|
if (!("idAttr" in aNounDef))
|
|
aNounDef.idAttr = "id";
|
|
if (!("comparator" in aNounDef)) {
|
|
aNounDef.comparator = function() {
|
|
throw new Error("Noun type '" + aNounDef.name +
|
|
"' lacks a real comparator.");
|
|
};
|
|
}
|
|
|
|
// We allow nouns to have data tables associated with them where we do all
|
|
// the legwork. The schema attribute is the gateway to this magical world
|
|
// of functionality. Said door is officially unsupported.
|
|
if (aNounDef.schema) {
|
|
if (!aNounDef.tableName) {
|
|
if (aNounDef.schema.name)
|
|
aNounDef.tableName = "ext_" + aNounDef.schema.name;
|
|
else
|
|
aNounDef.tableName = "ext_" + aNounDef.name;
|
|
}
|
|
// this creates the data table and binder and hooks everything up
|
|
GlodaDatastore.createNounTable(aNounDef);
|
|
|
|
if (!aNounDef.toParamAndValue)
|
|
aNounDef.toParamAndValue = function (aThing) {
|
|
if (aThing instanceof aNounDef.class)
|
|
return [null, aThing.id];
|
|
else // assume they're just passing the id directly
|
|
return [null, aThing];
|
|
};
|
|
}
|
|
|
|
// if it has a table, you can query on it. seems straight-forward.
|
|
if (aNounDef.tableName) {
|
|
[aNounDef.queryClass, aNounDef.nullQueryClass,
|
|
aNounDef.explicitQueryClass, aNounDef.wildcardQueryClass] =
|
|
GlodaQueryClassFactory(aNounDef);
|
|
aNounDef._dbMeta = {};
|
|
aNounDef.class.prototype.NOUN_ID = aNounDef.id;
|
|
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] = {};
|
|
|
|
return aNounDef;
|
|
},
|
|
|
|
/**
|
|
* 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: " +
|
|
Object.keys(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 nounDef.actions.
|
|
filter(action => !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",
|
|
clazz: Boolean, allowsArbitraryAttrs: false,
|
|
isPrimitive: true,
|
|
// favor true before false
|
|
comparator: function gloda_bool_comparator(a, b) {
|
|
if (a == null) {
|
|
if (b == null)
|
|
return 0;
|
|
else
|
|
return 1;
|
|
}
|
|
else if (b == null) {
|
|
return -1;
|
|
}
|
|
return b - a;
|
|
},
|
|
toParamAndValue: function(aBool) {
|
|
return [null, aBool ? 1 : 0];
|
|
}}, this.NOUN_BOOLEAN);
|
|
this.defineNoun({
|
|
name: "number",
|
|
clazz: Number, allowsArbitraryAttrs: false, continuous: true,
|
|
isPrimitive: true,
|
|
comparator: function gloda_number_comparator(a, b) {
|
|
if (a == null) {
|
|
if (b == null)
|
|
return 0;
|
|
else
|
|
return 1;
|
|
}
|
|
else if (b == null) {
|
|
return -1;
|
|
}
|
|
return a - b;
|
|
},
|
|
toParamAndValue: function(aNum) {
|
|
return [null, aNum];
|
|
}}, this.NOUN_NUMBER);
|
|
this.defineNoun({
|
|
name: "string",
|
|
clazz: String, allowsArbitraryAttrs: false,
|
|
isPrimitive: true,
|
|
comparator: function gloda_string_comparator(a, b) {
|
|
if (a == null) {
|
|
if (b == null)
|
|
return 0;
|
|
else
|
|
return 1;
|
|
}
|
|
else if (b == null) {
|
|
return -1;
|
|
}
|
|
return a.localeCompare(b);
|
|
},
|
|
toParamAndValue: function(aString) {
|
|
return [null, aString];
|
|
}}, this.NOUN_STRING);
|
|
this.defineNoun({
|
|
name: "date",
|
|
clazz: Date, allowsArbitraryAttrs: false, continuous: true,
|
|
isPrimitive: true,
|
|
comparator: function gloda_data_comparator(a, b) {
|
|
if (a == null) {
|
|
if (b == null)
|
|
return 0;
|
|
else
|
|
return 1;
|
|
}
|
|
else if (b == null) {
|
|
return -1;
|
|
}
|
|
return a - b;
|
|
},
|
|
toParamAndValue: function(aDate) {
|
|
return [null, aDate.valueOf() * 1000];
|
|
}}, this.NOUN_DATE);
|
|
this.defineNoun({
|
|
name: "fulltext",
|
|
clazz: String, allowsArbitraryAttrs: false, continuous: false,
|
|
isPrimitive: true,
|
|
comparator: function gloda_fulltext_comparator(a, b) {
|
|
throw new Error("Fulltext nouns are not comparable!");
|
|
},
|
|
// 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",
|
|
clazz: GlodaFolder,
|
|
allowsArbitraryAttrs: false,
|
|
isPrimitive: false,
|
|
queryHelpers: {
|
|
/**
|
|
* Query for accounts based on the account associated with folders. We
|
|
* walk all of the folders associated with an account and put them in
|
|
* the list of folders that match if gloda would index them. This is
|
|
* unsuitable for producing a persistable constraint since it does not
|
|
* adapt for added/deleted folders. However, it is sufficient for
|
|
* faceting. Also, we don't persist constraints yet.
|
|
*
|
|
* @TODO The long-term solution is to move towards using arithmetic
|
|
* encoding on folder-id's like we use for MIME types and friends.
|
|
*/
|
|
Account: function(aAttrDef, aArguments) {
|
|
let folderValues = [];
|
|
let seenRootFolders = {};
|
|
for (let iArg = 0; iArg < aArguments.length; iArg++) {
|
|
let givenFolder = aArguments[iArg];
|
|
let givenMsgFolder = givenFolder.getXPCOMFolder(
|
|
givenFolder.kActivityFolderOnlyNoData);
|
|
let rootFolder = givenMsgFolder.rootFolder;
|
|
|
|
// skip processing this folder if we have already processed its
|
|
// root folder.
|
|
if (rootFolder.URI in seenRootFolders)
|
|
continue;
|
|
seenRootFolders[rootFolder.URI] = true;
|
|
|
|
let allFolders = rootFolder.descendants;
|
|
for (let folder in fixIterator(allFolders, Ci.nsIMsgFolder)) {
|
|
let folderFlags = folder.flags;
|
|
|
|
// Ignore virtual folders, non-mail folders.
|
|
// XXX this is derived from GlodaIndexer's shouldIndexFolder.
|
|
// This should probably just use centralized code or the like.
|
|
if (!(folderFlags & Ci.nsMsgFolderFlags.Mail) ||
|
|
(folderFlags & Ci.nsMsgFolderFlags.Virtual))
|
|
continue;
|
|
// we only index local or IMAP folders
|
|
if (!(folder instanceof Ci.nsIMsgLocalMailFolder) &&
|
|
!(folder instanceof Ci.nsIMsgImapMailFolder))
|
|
continue;
|
|
|
|
let glodaFolder = Gloda.getFolderForFolder(folder);
|
|
folderValues.push(glodaFolder);
|
|
}
|
|
}
|
|
return this._inConstraintHelper(aAttrDef, folderValues);
|
|
}
|
|
},
|
|
comparator: function gloda_folder_comparator(a, b) {
|
|
if (a == null) {
|
|
if (b == null)
|
|
return 0;
|
|
else
|
|
return 1;
|
|
}
|
|
else if (b == null) {
|
|
return -1;
|
|
}
|
|
return a.name.localeCompare(b.name);
|
|
},
|
|
toParamAndValue: function(aFolderOrGlodaFolder) {
|
|
if (aFolderOrGlodaFolder instanceof GlodaFolder)
|
|
return [null, aFolderOrGlodaFolder.id];
|
|
else
|
|
return [null, GlodaDatastore._mapFolder(aFolderOrGlodaFolder).id];
|
|
}}, this.NOUN_FOLDER);
|
|
this.defineNoun({
|
|
name: "account",
|
|
clazz: GlodaAccount,
|
|
allowsArbitraryAttrs: false,
|
|
isPrimitive: false,
|
|
equals: function(a, b) {
|
|
if (a && !b || !a && b)
|
|
return false;
|
|
if (!a && !b)
|
|
return true;
|
|
return a.id == b.id;
|
|
},
|
|
comparator: function gloda_account_comparator(a, b) {
|
|
if (a == null) {
|
|
if (b == null)
|
|
return 0;
|
|
else
|
|
return 1;
|
|
}
|
|
else if (b == null) {
|
|
return -1;
|
|
}
|
|
return a.name.localeCompare(b.name);
|
|
}}, this.NOUN_ACCOUNT);
|
|
this.defineNoun({
|
|
name: "conversation",
|
|
clazz: GlodaConversation,
|
|
allowsArbitraryAttrs: false,
|
|
isPrimitive: false,
|
|
cache: true, cacheCost: 512,
|
|
tableName: "conversations",
|
|
attrTableName: "messageAttributes", attrIDColumnName: "conversationID",
|
|
datastore: GlodaDatastore,
|
|
objFromRow: GlodaDatastore._conversationFromRow,
|
|
comparator: function gloda_conversation_comparator(a, b) {
|
|
if (a == null) {
|
|
if (b == null)
|
|
return 0;
|
|
else
|
|
return 1;
|
|
}
|
|
else if (b == null) {
|
|
return -1;
|
|
}
|
|
return a.subject.localeCompare(b.subject);
|
|
},
|
|
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",
|
|
clazz: GlodaMessage,
|
|
allowsArbitraryAttrs: true,
|
|
isPrimitive: false,
|
|
cache: true, cacheCost: 2048,
|
|
tableName: "messages",
|
|
// we will always have a fulltext row, even for messages where we don't
|
|
// have the body available. this is because we want the subject indexed.
|
|
dbQueryJoinMagic:
|
|
" INNER JOIN messagesText ON messages.id = messagesText.rowid",
|
|
attrTableName: "messageAttributes", attrIDColumnName: "messageID",
|
|
datastore: GlodaDatastore, objFromRow: GlodaDatastore._messageFromRow,
|
|
dbAttribAdjuster: GlodaDatastore.adjustMessageAttributes,
|
|
dbQueryValidityConstraintSuffix:
|
|
" AND +deleted = 0 AND +folderID IS NOT NULL AND +messageKey IS NOT NULL",
|
|
// This is what's used when we have no validity constraints, i.e. we allow
|
|
// for ghost messages, which do not have a row in the messagesText table.
|
|
dbQueryJoinMagicWithNoValidityConstraints:
|
|
" LEFT JOIN messagesText ON messages.id = messagesText.rowid",
|
|
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",
|
|
clazz: GlodaContact,
|
|
allowsArbitraryAttrs: true,
|
|
isPrimitive: false,
|
|
cache: true, cacheCost: 128,
|
|
tableName: "contacts",
|
|
attrTableName: "contactAttributes", attrIDColumnName: "contactID",
|
|
datastore: GlodaDatastore, objFromRow: GlodaDatastore._contactFromRow,
|
|
dbAttribAdjuster: GlodaDatastore.adjustAttributes,
|
|
objInsert: GlodaDatastore.insertContact,
|
|
objUpdate: GlodaDatastore.updateContact,
|
|
comparator: function gloda_contact_comparator(a, b) {
|
|
if (a == null) {
|
|
if (b == null)
|
|
return 0;
|
|
else
|
|
return 1;
|
|
}
|
|
else if (b == null) {
|
|
return -1;
|
|
}
|
|
return a.name.localeCompare(b.name);
|
|
},
|
|
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",
|
|
clazz: GlodaIdentity,
|
|
allowsArbitraryAttrs: false,
|
|
isPrimitive: false,
|
|
cache: true, cacheCost: 128,
|
|
usesUniqueValue: true,
|
|
tableName: "identities",
|
|
datastore: GlodaDatastore, objFromRow: GlodaDatastore._identityFromRow,
|
|
/**
|
|
* Short string is the contact name, long string includes the identity
|
|
* value too, delimited by a colon. Not tremendously localizable.
|
|
*/
|
|
userVisibleString: function(aIdentity, aLong) {
|
|
if (!aLong)
|
|
return aIdentity.contact.name;
|
|
if (aIdentity.contact.name == aIdentity.value)
|
|
return aIdentity.value;
|
|
return aIdentity.contact.name + " (" + aIdentity.value + ")";
|
|
},
|
|
comparator: function gloda_identity_comparator(a, b) {
|
|
if (a == null) {
|
|
if (b == null)
|
|
return 0;
|
|
else
|
|
return 1;
|
|
}
|
|
else if (b == null) {
|
|
return -1;
|
|
}
|
|
return a.contact.name.localeCompare(b.contact.name);
|
|
},
|
|
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);
|
|
this.defineNoun({
|
|
name: "attachment-infos",
|
|
clazz: GlodaAttachment,
|
|
allowsArbitraryAttrs: false,
|
|
isPrimitive: false,
|
|
toJSON: function (x) {
|
|
return [
|
|
x._name,
|
|
x._contentType,
|
|
x._size,
|
|
x._part,
|
|
x._externalUrl,
|
|
x._isExternal
|
|
]
|
|
},
|
|
fromJSON: function (x, aGlodaMessage) {
|
|
let [name, contentType, size, _part, _externalUrl, isExternal] = x;
|
|
return new GlodaAttachment(aGlodaMessage, name, contentType, size, _part, _externalUrl, isExternal);
|
|
},
|
|
}, this.NOUN_ATTACHMENT);
|
|
|
|
// 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",
|
|
clazz: null,
|
|
allowsArbitraryAttrs: false,
|
|
comparator: function gloda_fulltext_comparator(a, b) {
|
|
if (a == null) {
|
|
if (b == null)
|
|
return 0;
|
|
else
|
|
return 1;
|
|
}
|
|
else if (b == null) {
|
|
return -1;
|
|
}
|
|
// First sort by the first identity in the tuple
|
|
// Since our general use-case is for the first guy to be "me", we only
|
|
// compare the identity value, not the name.
|
|
let fic = a[0].value.localeCompare(b[0].value);
|
|
if (fic)
|
|
return fic;
|
|
// Next compare the second identity in the tuple, but use the contact
|
|
// this time to be consistent with our identity comparator.
|
|
return a[1].contact.name.localeCompare(b[1].contact.name);
|
|
},
|
|
computeDelta: function(aCurValues, aOldValues) {
|
|
let oldMap = {};
|
|
for (let tupe of aOldValues) {
|
|
let [originIdentity, targetIdentity] = tupe;
|
|
let targets = oldMap[originIdentity];
|
|
if (targets === undefined)
|
|
targets = oldMap[originIdentity] = {};
|
|
targets[targetIdentity] = true;
|
|
}
|
|
|
|
let added = [], removed = [];
|
|
for (let tupe of aCurValues) {
|
|
let [originIdentity, targetIdentity] = tupe;
|
|
let targets = oldMap[originIdentity];
|
|
if ((targets === undefined) || !(targetIdentity in targets))
|
|
added.push(tupe);
|
|
else
|
|
delete targets[targetIdentity];
|
|
}
|
|
|
|
for (let originIdentity in oldMap) {
|
|
let targets = oldMap[originIdentity];
|
|
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 (let tupe of 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 (let tupe of 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;
|
|
let canQuery = true;
|
|
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.canQuery || aAttrDef.attributeName.startsWith("_")) {
|
|
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 {
|
|
constrainer = function() {
|
|
throw new Error(
|
|
"Cannot query on attribute "+aAttrDef.attributeName
|
|
+ " because its canQuery parameter hasn't been set to true."
|
|
+ " Reading the comments about Gloda.defineAttribute may be a"
|
|
+ " sensible thing to do now.");
|
|
}
|
|
canQuery = false;
|
|
}
|
|
|
|
aSubjectNounDef.queryClass.prototype[aAttrDef.boundName] = constrainer;
|
|
|
|
// Don't bind extra query-able attributes if we're unable to perform a
|
|
// search on the attribute.
|
|
if (!canQuery)
|
|
return;
|
|
|
|
// - 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(constraint);
|
|
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;
|
|
}
|
|
|
|
// - Custom helpers provided by the noun type...
|
|
if ("queryHelpers" in objectNounDef) {
|
|
for (let name in objectNounDef.queryHelpers) {
|
|
let helper = objectNounDef.queryHelpers[name];
|
|
// we need a new closure...
|
|
let helperFunc = helper;
|
|
aSubjectNounDef.queryClass.prototype[aAttrDef.boundName + name] =
|
|
function() {
|
|
return helperFunc.call(this, aAttrDef, arguments);
|
|
};
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Names of attribute-specific localized strings and the JS attribute they are
|
|
* exposed as in the attribute's "strings" attribute (if the provider has a
|
|
* string bundle exposed on its "strings" attribute). They are rooted at
|
|
* "gloda.SUBJECT-NOUN-NAME.attr.ATTR-NAME.*".
|
|
*
|
|
* Please consult the localization notes in gloda.properties to understand
|
|
* what these are used for.
|
|
*/
|
|
_ATTR_LOCALIZED_STRINGS: {
|
|
/* - Faceting */
|
|
facetNameLabel: "facetNameLabel",
|
|
includeLabel: "includeLabel",
|
|
excludeLabel: "excludeLabel",
|
|
remainderLabel: "remainderLabel",
|
|
mustMatchLabel: "mustMatchLabel",
|
|
cantMatchLabel: "cantMatchLabel",
|
|
mayMatchLabel: "mayMatchLabel",
|
|
mustMatchNoneLabel: "mustMatchNoneLabel",
|
|
mustMatchSomeLabel: "mustMatchSomeLabel",
|
|
mayMatchAnyLabel: "mayMatchAnyLabel",
|
|
},
|
|
/**
|
|
* Define an attribute and all its meta-data. Takes a single dictionary as
|
|
* its argument, with the following required properties:
|
|
*
|
|
* @param aAttrDef.provider The object instance providing a 'process' method.
|
|
* @param aAttrDef.extensionName The name of the extension providing these
|
|
* attributes.
|
|
* @param aAttrDef.attributeType The type of attribute, one of the values from
|
|
* the kAttr* enumeration.
|
|
* @param aAttrDef.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 aAttrDef.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 aAttrDef.bindName Optional override of attributeName for purposes of
|
|
* the binding property's name.
|
|
* @param aAttrDef.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 [aAttrDef.emptySetIsSignificant=false] Should we
|
|
* @param aAttrDef.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 aAttrDef.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.");
|
|
|
|
// -- Fill in defaults
|
|
if (!("emptySetIsSignificant" in aAttrDef))
|
|
aAttrDef.emptySetIsSignificant = false;
|
|
|
|
if (!("canQuery" in aAttrDef))
|
|
aAttrDef.canQuery = aAttrDef.facet ? true : false;
|
|
|
|
// return if the attribute has already been defined
|
|
if (aAttrDef.dbDef)
|
|
return aAttrDef;
|
|
|
|
// - first time we've seen a provider init logic
|
|
if (!(aAttrDef.provider.providerName in this._attrProviders)) {
|
|
this._attrProviders[aAttrDef.provider.providerName] = [];
|
|
if (aAttrDef.provider.contentWhittle)
|
|
whittlerRegistry.registerWhittler(aAttrDef.provider);
|
|
}
|
|
|
|
let compoundName = aAttrDef.extensionName + ":" + aAttrDef.attributeName;
|
|
// -- Database Definition
|
|
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);
|
|
|
|
// -- Facets
|
|
function normalizeFacetDef(aFacetDef) {
|
|
if (!("groupIdAttr" in aFacetDef))
|
|
aFacetDef.groupIdAttr = aAttrDef.objectNounDef.idAttr;
|
|
if (!("groupComparator" in aFacetDef))
|
|
aFacetDef.groupComparator = aAttrDef.objectNounDef.comparator;
|
|
if (!("filter" in aFacetDef))
|
|
aFacetDef.filter = null;
|
|
}
|
|
// No facet attribute means no facet desired; set an explicit null so that
|
|
// code can check without doing an "in" check.
|
|
if (!("facet" in aAttrDef))
|
|
aAttrDef.facet = null;
|
|
// Promote "true" facet values to the defaults. Where attributes have
|
|
// specified values, make sure we fill in any missing defaults.
|
|
else {
|
|
if (aAttrDef.facet == true) {
|
|
aAttrDef.facet = {
|
|
type: "default",
|
|
groupIdAttr: aAttrDef.objectNounDef.idAttr,
|
|
groupComparator: aAttrDef.objectNounDef.comparator,
|
|
filter: null,
|
|
};
|
|
}
|
|
else {
|
|
normalizeFacetDef(aAttrDef.facet);
|
|
}
|
|
}
|
|
if ("extraFacets" in aAttrDef) {
|
|
for (let facetDef of aAttrDef.extraFacets) {
|
|
normalizeFacetDef(facetDef);
|
|
}
|
|
}
|
|
|
|
function gatherLocalizedStrings(aBundle, aPropRoot, aStickIn) {
|
|
for (let propName in Gloda._ATTR_LOCALIZED_STRINGS) {
|
|
let attrName = Gloda._ATTR_LOCALIZED_STRINGS[propName];
|
|
try {
|
|
aStickIn[attrName] = aBundle.get(aPropRoot + propName);
|
|
}
|
|
catch (ex) {
|
|
// do nothing. nsIStringBundle throws exceptions because it is a
|
|
// standard nsresult type of API and our helper buddy does nothing
|
|
// to help us. (StringBundle.js, that is.)
|
|
}
|
|
}
|
|
}
|
|
|
|
// -- L10n.
|
|
// If the provider has a string bundle, populate a "strings" attribute with
|
|
// our standard attribute strings that can be UI exposed.
|
|
if (("strings" in aAttrDef.provider) && (aAttrDef.facet)) {
|
|
let bundle = aAttrDef.provider.strings;
|
|
|
|
// -- attribute strings
|
|
let attrStrings = aAttrDef.facet.strings = {};
|
|
// we use the first subject the attribute applies to as the basis of
|
|
// where to get the string from. Mainly because we currently don't have
|
|
// any attributes with multiple subjects nor a use-case where we expose
|
|
// multiple noun types via the UI. (Just messages right now.)
|
|
let canonicalSubject = this._nounIDToDef[aAttrDef.subjectNouns[0]];
|
|
let propRoot = "gloda." + canonicalSubject.name + ".attr." +
|
|
aAttrDef.attributeName + ".";
|
|
gatherLocalizedStrings(bundle, propRoot, attrStrings);
|
|
|
|
// -- alias strings for synthetic facets
|
|
if ("extraFacets" in aAttrDef) {
|
|
for (let facetDef of aAttrDef.extraFacets) {
|
|
facetDef.strings = {};
|
|
let aliasPropRoot = "gloda." + canonicalSubject.name + ".attr." +
|
|
facetDef.alias + ".";
|
|
gatherLocalizedStrings(bundle, aliasPropRoot, facetDef.strings);
|
|
}
|
|
}
|
|
}
|
|
|
|
// -- Subject Noun Binding
|
|
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];
|
|
},
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @param aNounID The (integer) noun-id of the noun you want to query on.
|
|
* @param aOptions an optional dictionary of query options, see the GlodaQuery
|
|
* class documentation.
|
|
*/
|
|
newQuery: function gloda_ns_newQuery(aNounID, aOptions) {
|
|
let nounDef = this._nounIDToDef[aNounID];
|
|
return new nounDef.queryClass(aOptions);
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
},
|
|
|
|
/**
|
|
* Attribute providers attempting to index something that experience a fatal
|
|
* problem should throw one of these. For example:
|
|
* "throw new Gloda.BadItemContentsError('Message lacks an author.');".
|
|
*
|
|
* We're not really taking advantage of this yet, but it's a good idea.
|
|
*/
|
|
BadItemContentsError: BadItemContentsError,
|
|
|
|
/**
|
|
* Populate a gloda representation of an item given the thus-far built
|
|
* representation, the previous representation, and one or more raw
|
|
* representations. The attribute providers/optimizers for the given noun
|
|
* type are invoked, allowing them to contribute/alter things. Following
|
|
* that, we build and persist our attribute 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.
|
|
*
|
|
* @param aItem The noun instance you want processed.
|
|
* @param aRawReps A dictionary that we pass to the attribute providers.
|
|
* There is a(n implied) contract between the caller of grokNounItem for a
|
|
* given noun type and the attribute providers for that noun type, and we
|
|
* have nothing to do with it OTHER THAN inserting a 'trueGlodaRep'
|
|
* value into it. In the event of reindexing an existing object, the
|
|
* gloda representation we pass to the indexers is actually a clone that
|
|
* allows the asynchronous indexers to mutate the object without
|
|
* causing visible changes in the existing representation of the gloda
|
|
* object. We patch the changes back onto the original item atomically
|
|
* once indexing completes. The 'trueGlodaRep' is then useful for
|
|
* objects that hang off of the gloda instance that need a reference
|
|
* back to their containing object for API convenience purposes.
|
|
* @param aIsConceptuallyNew Is the item "new" in the sense that it would
|
|
* never have been visible from within user code? This translates into
|
|
* whether this should trigger an itemAdded notification or an
|
|
* itemModified notification.
|
|
* @param aIsRecordNew Is the item "new" in the sense that we should INSERT
|
|
* a record rather than UPDATE-ing a record. For example, when dealing
|
|
* with messages where we may have a ghost, the ghost message is not a
|
|
* new record, but is conceptually new.
|
|
* @param aCallbackHandle The GlodaIndexer-style callback handle that is being
|
|
* used to drive this processing in an async fashion. (See
|
|
* GlodaIndexer._callbackHandle).
|
|
* @param aDoCache Should we allow this item to be contributed to its noun
|
|
* cache?
|
|
*/
|
|
grokNounItem: function* gloda_ns_grokNounItem(aItem, aRawReps,
|
|
aIsConceptuallyNew, aIsRecordNew, aCallbackHandle, aDoCache) {
|
|
let itemNounDef = aItem.NOUN_DEF;
|
|
let attribsByBoundName = itemNounDef.attribsByBoundName;
|
|
|
|
this._log.info(" ** grokNounItem: " + itemNounDef.name);
|
|
|
|
let addDBAttribs = [];
|
|
let removeDBAttribs = [];
|
|
|
|
let jsonDict = {};
|
|
|
|
let aOldItem;
|
|
aRawReps.trueGlodaRep = aItem;
|
|
if (aIsConceptuallyNew) // 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[itemNounDef.id];
|
|
for (let iProvider = 0; iProvider < attrProviders.length; iProvider++) {
|
|
this._log.info(" * provider: " + attrProviders[iProvider].providerName);
|
|
yield aCallbackHandle.pushAndGo(
|
|
attrProviders[iProvider].process(aItem, aRawReps, aIsConceptuallyNew,
|
|
aCallbackHandle));
|
|
}
|
|
|
|
let attrOptimizers = this._attrOptimizerOrderByNoun[itemNounDef.id];
|
|
for (let iProvider = 0; iProvider < attrOptimizers.length; iProvider++) {
|
|
this._log.info(" * optimizer: " + attrOptimizers[iProvider].providerName);
|
|
yield aCallbackHandle.pushAndGo(
|
|
attrOptimizers[iProvider].optimize(aItem, aRawReps, aIsConceptuallyNew,
|
|
aCallbackHandle));
|
|
}
|
|
this._log.info(" ** done with providers.");
|
|
|
|
// Iterate over the attributes on the item
|
|
for (let key of Object.keys(aItem)) {
|
|
let value = aItem[key];
|
|
// 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.startsWith("_"))
|
|
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) {
|
|
let toJSON = objectNounDef.toJSON;
|
|
jsonDict[attrib.id] = [];
|
|
for (let [, subValue] in Iterator(value)) {
|
|
jsonDict[attrib.id].push(toJSON(subValue));
|
|
}
|
|
}
|
|
else
|
|
jsonDict[attrib.id] = value;
|
|
}
|
|
|
|
let oldValue = aOldItem[key];
|
|
|
|
// the 'old' item is still the canonical one; update it
|
|
// do the update now, because we may skip operations on addDBAttribs and
|
|
// removeDBattribs, if the attribute is not to generate entries in
|
|
// messageAttributes
|
|
if (oldValue !== undefined || !aIsConceptuallyNew)
|
|
aOldItem[key] = value;
|
|
|
|
// the new canQuery property has to be set to true to generate entries
|
|
// in the messageAttributes table. Any other truthy value (like a non
|
|
// empty string), will still make the message query-able but without
|
|
// using the database.
|
|
if (attrib.canQuery !== true) {
|
|
continue;
|
|
}
|
|
|
|
// - database index attributes
|
|
|
|
// perform a delta analysis against the old value, if we have one
|
|
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))) {
|
|
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
|
|
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 (let anOldValue of oldValue) {
|
|
// 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 (let curValue of value) {
|
|
if (curValue in oldValueMap)
|
|
delete oldValueMap[curValue];
|
|
else
|
|
valuesAdded.push(curValue);
|
|
}
|
|
// anything still on oldValueMap was removed.
|
|
let valuesRemoved = Object.keys(oldValueMap).
|
|
map(key => oldValueMap[key]);
|
|
// convert the values to database-style attribute rows
|
|
addDBAttribs.push.apply(addDBAttribs,
|
|
attribDB.convertValuesToDBAttributes(valuesAdded));
|
|
removeDBAttribs.push.apply(removeDBAttribs,
|
|
attribDB.convertValuesToDBAttributes(valuesRemoved));
|
|
}
|
|
|
|
// Add/remove the empty set indicator as appropriate.
|
|
if (attrib.emptySetIsSignificant) {
|
|
// if we are now non-zero but previously were zero, remove.
|
|
if (value.length && !oldValue.length)
|
|
removeDBAttribs.push([GlodaDatastore.kEmptySetAttrId, attribDB.id]);
|
|
// if we are now zero length but previously were not, add
|
|
else if (!value.length && oldValue.length)
|
|
addDBAttribs.push([GlodaDatastore.kEmptySetAttrId, attribDB.id]);
|
|
}
|
|
}
|
|
// no old value, all values are new
|
|
else {
|
|
// add the db reps on the new values
|
|
if (attrib.singular)
|
|
value = [value];
|
|
addDBAttribs.push.apply(addDBAttribs,
|
|
attribDB.convertValuesToDBAttributes(value));
|
|
// Add the empty set indicator for the attribute id if appropriate.
|
|
if (!value.length && attrib.emptySetIsSignificant)
|
|
addDBAttribs.push([GlodaDatastore.kEmptySetAttrId, attribDB.id]);
|
|
}
|
|
}
|
|
|
|
// Iterate over any remaining values in old items for purge purposes.
|
|
for (let key of Object.keys(aOldItem)) {
|
|
let value = aOldItem[key];
|
|
// 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.startsWith("_"))
|
|
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) {
|
|
continue;
|
|
}
|
|
|
|
// delete these from the old item, as the old item is canonical, and
|
|
// should no longer have these values
|
|
delete aOldItem[key];
|
|
|
|
if (attrib.canQuery !== true) {
|
|
this._log.debug("Not inserting attribute "+attrib.attributeName
|
|
+" into the db, since we don't plan on querying on it");
|
|
continue;
|
|
}
|
|
|
|
if (attrib.singular)
|
|
value = [value];
|
|
let attribDB = attrib.dbDef;
|
|
removeDBAttribs.push.apply(removeDBAttribs,
|
|
attribDB.convertValuesToDBAttributes(value));
|
|
// remove the empty set marker if there should have been one
|
|
if (!value.length && attrib.emptySetIsSignificant)
|
|
removeDBAttribs.push([GlodaDatastore.kEmptySetAttrId, attribDB.id]);
|
|
}
|
|
|
|
aItem._jsonText = JSON.stringify(jsonDict);
|
|
this._log.debug(" json text: " + aItem._jsonText);
|
|
|
|
if (aIsRecordNew) {
|
|
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);
|
|
|
|
if (!aIsConceptuallyNew && ("_declone" in aOldItem))
|
|
aOldItem._declone(aItem);
|
|
|
|
// Cache ramifications...
|
|
if (aDoCache === undefined || aDoCache) {
|
|
if (aIsConceptuallyNew)
|
|
GlodaCollectionManager.itemsAdded(aItem.NOUN_ID, [aItem]);
|
|
else
|
|
GlodaCollectionManager.itemsModified(aOldItem.NOUN_ID, [aOldItem]);
|
|
}
|
|
|
|
this._log.debug(" done grokking.");
|
|
|
|
yield this.kWorkDone;
|
|
},
|
|
|
|
/**
|
|
* Processes a list of noun instances for their score within a given context.
|
|
* This is primarily intended for use by search ranking mechanisms, but could
|
|
* be used elsewhere too. (It does, however, depend on the complicity of the
|
|
* score method implementations to not get confused.)
|
|
*
|
|
* @param aItems The non-empty list of items to score.
|
|
* @param aContext A noun-specific dictionary that we just pass to the funcs.
|
|
* @param aExtraScoreFuncs A list of extra scoring functions to apply.
|
|
* @returns A list of integer scores equal in length to aItems.
|
|
*/
|
|
scoreNounItems: function gloda_ns_grokNounItem(aItems, aContext,
|
|
aExtraScoreFuncs) {
|
|
let scores = [];
|
|
// bail if there is nothing to score
|
|
if (!aItems.length)
|
|
return scores;
|
|
|
|
let itemNounDef = aItems[0].NOUN_DEF;
|
|
if (aExtraScoreFuncs == null)
|
|
aExtraScoreFuncs = [];
|
|
|
|
for (let item of aItems) {
|
|
let score = 0;
|
|
let attrProviders = this._attrProviderOrderByNoun[itemNounDef.id];
|
|
for (let iProvider = 0; iProvider < attrProviders.length; iProvider++) {
|
|
let provider = attrProviders[iProvider];
|
|
if (provider.score)
|
|
score += provider.score(item);
|
|
}
|
|
for (let [, extraScoreFunc] in Iterator(aExtraScoreFuncs))
|
|
score += extraScoreFunc(item, aContext);
|
|
scores.push(score);
|
|
}
|
|
|
|
return scores;
|
|
}
|
|
};
|
|
|
|
/* and initialize the Gloda object/NS before we return... */
|
|
try {
|
|
Gloda._init();
|
|
}
|
|
catch (ex) {
|
|
Gloda._log.debug("Exception during Gloda init (" + ex.fileName + ":" +
|
|
ex.lineNumber + "): " + ex);
|
|
};
|
|
/* but don't forget that we effectively depend on everybody.js too, and
|
|
currently on our importer to be importing that if they need us fully armed
|
|
and operational. */
|